diff --git a/.github/workflows/basic.yml b/.github/workflows/basic.yml index 321db470c55..0210372b6b5 100644 --- a/.github/workflows/basic.yml +++ b/.github/workflows/basic.yml @@ -1,6 +1,7 @@ name: Basic checks on: workflow_dispatch +permissions: {} # on: # push: @@ -20,6 +21,8 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - uses: actions/checkout@v4 + with: + persist-credentials: false # TODO: rename azure-pipelines/linux/xvfb.init to github-actions - name: Setup Build Environment @@ -80,6 +83,8 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: actions/setup-node@v4 with: @@ -118,8 +123,8 @@ jobs: - name: Run Valid Layers Checks run: npm run valid-layers-check - - name: Run Property Init Order Checks - run: npm run property-init-order-check + - name: Run Define Class Fields Checks + run: npm run define-class-fields-check - name: Compile /build/ run: npm run compile @@ -146,6 +151,8 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: actions/setup-node@v4 with: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 40dca0aaefb..3d5e483ac34 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,7 @@ name: CI on: workflow_dispatch +permissions: {} # on: # push: @@ -21,6 +22,8 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: actions/setup-node@v4 with: @@ -103,6 +106,8 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - uses: actions/checkout@v4 + with: + persist-credentials: false # TODO: rename azure-pipelines/linux/xvfb.init to github-actions - name: Setup Build Environment @@ -185,6 +190,8 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: actions/setup-node@v4 with: @@ -258,6 +265,8 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: actions/setup-node@v4 with: @@ -299,8 +308,8 @@ jobs: - name: Run Valid Layers Checks run: npm run valid-layers-check - - name: Run Property Init Order Checks - run: npm run property-init-order-check + - name: Run Define Class Fields Checks + run: npm run define-class-fields-check - name: Compile /build/ run: npm run compile diff --git a/.github/workflows/monaco-editor.yml b/.github/workflows/monaco-editor.yml index 2f32abb59b0..56c30d0ba74 100644 --- a/.github/workflows/monaco-editor.yml +++ b/.github/workflows/monaco-editor.yml @@ -9,6 +9,7 @@ on: branches: - main - release/* +permissions: {} jobs: main: @@ -19,6 +20,8 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: actions/setup-node@v4 with: diff --git a/.github/workflows/no-package-lock-changes.yml b/.github/workflows/no-package-lock-changes.yml index 45d5d17407b..059b1c2115f 100644 --- a/.github/workflows/no-package-lock-changes.yml +++ b/.github/workflows/no-package-lock-changes.yml @@ -1,12 +1,14 @@ name: Prevent package-lock.json changes in PRs -on: [pull_request] + +on: pull_request +permissions: {} jobs: main: name: Prevent package-lock.json changes in PRs runs-on: ubuntu-latest steps: - - uses: octokit/request-action@v2.x + - uses: octokit/request-action@dad4362715b7fb2ddedf9772c8670824af564f0d # v2.4.0 id: get_permissions with: route: GET /repos/microsoft/vscode/collaborators/{username}/permission diff --git a/.github/workflows/no-yarn-lock-changes.yml b/.github/workflows/no-yarn-lock-changes.yml index 57082a28b1c..fd643cd56a9 100644 --- a/.github/workflows/no-yarn-lock-changes.yml +++ b/.github/workflows/no-yarn-lock-changes.yml @@ -1,12 +1,14 @@ name: Prevent yarn.lock changes in PRs -on: [pull_request] + +on: pull_request +permissions: {} jobs: main: name: Prevent yarn.lock changes in PRs runs-on: ubuntu-latest steps: - - uses: octokit/request-action@v2.x + - uses: octokit/request-action@dad4362715b7fb2ddedf9772c8670824af564f0d # v2.4.0 id: get_permissions with: route: GET /repos/microsoft/vscode/collaborators/{username}/permission @@ -22,7 +24,7 @@ jobs: echo "should_run: ${{ !contains(fromJson('["admin", "maintain", "write"]'), fromJson(steps.get_permissions.outputs.data).permission) }}" echo "should_run=${{ !contains(fromJson('["admin", "maintain", "write"]'), fromJson(steps.get_permissions.outputs.data).permission) && github.event.pull_request.user.login != 'dependabot[bot]' }}" >> $GITHUB_OUTPUT - name: Get file changes - uses: trilom/file-changes-action@ce38c8ce2459ca3c303415eec8cb0409857b4272 + uses: trilom/file-changes-action@a6ca26c14274c33b15e6499323aac178af06ad4b # v1.2.4 if: ${{ steps.control.outputs.should_run == 'true' }} - name: Check for lockfile changes if: ${{ steps.control.outputs.should_run == 'true' }} diff --git a/.github/workflows/telemetry.yml b/.github/workflows/telemetry.yml index a5ac3be4198..84a2ffaaf93 100644 --- a/.github/workflows/telemetry.yml +++ b/.github/workflows/telemetry.yml @@ -1,13 +1,15 @@ name: 'Telemetry' -on: - pull_request: +on: pull_request +permissions: {} jobs: - check-metdata: + check-metadata: name: 'Check metadata' runs-on: 'ubuntu-latest' steps: - uses: 'actions/checkout@v4' + with: + persist-credentials: false - uses: 'actions/setup-node@v4' with: diff --git a/.npmrc b/.npmrc index 05f84f8a2ad..a9265068a13 100644 --- a/.npmrc +++ b/.npmrc @@ -1,6 +1,6 @@ disturl="https://electronjs.org/headers" -target="34.5.1" -ms_build_id="11369351" +target="35.2.2" +ms_build_id="11520120" runtime="electron" build_from_source="true" legacy-peer-deps="true" diff --git a/.nvmrc b/.nvmrc index 5bd6811705e..7d41c735d71 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20.19.0 +22.14.0 diff --git a/.vscode/extensions/vscode-selfhost-test-provider/package-lock.json b/.vscode/extensions/vscode-selfhost-test-provider/package-lock.json index 342512be8a9..65c729b92b8 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/package-lock.json +++ b/.vscode/extensions/vscode-selfhost-test-provider/package-lock.json @@ -16,7 +16,7 @@ }, "devDependencies": { "@types/mocha": "^10.0.6", - "@types/node": "20.x" + "@types/node": "22.x" }, "engines": { "vscode": "^1.88.0" @@ -56,13 +56,13 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.17.27", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.27.tgz", - "integrity": "sha512-U58sbKhDrthHlxHRJw7ZLiLDZGmAUOZUbpw0S6nL27sYUdhvgBLCRu/keSd6qcTsfArd1sRFCCBxzWATGr/0UA==", + "version": "22.13.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", + "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.19.2" + "undici-types": "~6.20.0" } }, "node_modules/ansi-styles": { @@ -93,9 +93,9 @@ } }, "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "dev": true, "license": "MIT" } diff --git a/.vscode/extensions/vscode-selfhost-test-provider/package.json b/.vscode/extensions/vscode-selfhost-test-provider/package.json index 71b5ee73c60..ec4ea96389b 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/package.json +++ b/.vscode/extensions/vscode-selfhost-test-provider/package.json @@ -78,7 +78,7 @@ }, "devDependencies": { "@types/mocha": "^10.0.6", - "@types/node": "20.x" + "@types/node": "22.x" }, "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", diff --git a/build/.cachesalt b/build/.cachesalt index 2b7a6b46c22..2d02e2ba768 100644 --- a/build/.cachesalt +++ b/build/.cachesalt @@ -1 +1 @@ -2025-04-08T11:12:10.188Z +2025-05-14T16:33:46.494Z diff --git a/build/azure-pipelines/linux/product-build-linux.yml b/build/azure-pipelines/linux/product-build-linux.yml index 96f76f6846a..dcf3964e056 100644 --- a/build/azure-pipelines/linux/product-build-linux.yml +++ b/build/azure-pipelines/linux/product-build-linux.yml @@ -117,7 +117,7 @@ steps: displayName: Install build dependencies condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) - # Step will be used by both Install dependencies and building rpm package, + # Step will be used by both verify glibcxx version for remote server and building rpm package, # hence avoid adding it behind NODE_MODULES_RESTORED condition. - script: | set -e @@ -125,35 +125,13 @@ steps: if [ "$SYSROOT_ARCH" == "x64" ]; then SYSROOT_ARCH="amd64" fi - export VSCODE_SYSROOT_DIR=$(Build.SourcesDirectory)/.build/sysroots - SYSROOT_ARCH="$SYSROOT_ARCH" node -e '(async () => { const { getVSCodeSysroot } = require("./build/linux/debian/install-sysroot.js"); await getVSCodeSysroot(process.env["SYSROOT_ARCH"]); })()' + export VSCODE_SYSROOT_DIR=$(Build.SourcesDirectory)/.build/sysroots/glibc-2.28-gcc-8.5.0 + SYSROOT_ARCH="$SYSROOT_ARCH" VSCODE_SYSROOT_PREFIX="-glibc-2.28-gcc-8.5.0" node -e '(async () => { const { getVSCodeSysroot } = require("./build/linux/debian/install-sysroot.js"); await getVSCodeSysroot(process.env["SYSROOT_ARCH"]); })()' env: VSCODE_ARCH: $(VSCODE_ARCH) GITHUB_TOKEN: "$(github-distro-mixin-password)" displayName: Download vscode sysroots - - ${{ if or(eq(parameters.VSCODE_ARCH, 'arm64'), eq(parameters.VSCODE_ARCH, 'armhf')) }}: - - script: | - set -e - includes=$(cat << 'EOF' - { - "target_defaults": { - "conditions": [ - ["OS=='linux'", { - 'cflags_cc!': [ '-std=gnu++20' ], - 'cflags_cc': [ '-std=gnu++2a' ], - }] - ] - } - } - EOF - ) - if [ ! -d "$HOME/.gyp" ]; then - mkdir -p "$HOME/.gyp" - fi - echo "$includes" > "$HOME/.gyp/include.gypi" - displayName: Override gnu target for arm64 and arm - - script: | set -e @@ -261,6 +239,7 @@ steps: EXPECTED_GLIBC_VERSION="2.28" \ EXPECTED_GLIBCXX_VERSION="3.4.25" \ + VSCODE_SYSROOT_DIR="$(Build.SourcesDirectory)/.build/sysroots/glibc-2.28-gcc-8.5.0" \ ./build/azure-pipelines/linux/verify-glibc-requirements.sh env: SEARCH_PATH: $(SERVER_UNARCHIVE_PATH) @@ -274,6 +253,7 @@ steps: EXPECTED_GLIBC_VERSION="2.28" \ EXPECTED_GLIBCXX_VERSION="3.4.26" \ + VSCODE_SYSROOT_DIR="$(Build.SourcesDirectory)/.build/sysroots/glibc-2.28-gcc-8.5.0" \ ./build/azure-pipelines/linux/verify-glibc-requirements.sh env: SEARCH_PATH: $(SERVER_UNARCHIVE_PATH) @@ -316,7 +296,7 @@ steps: elif [ "$VSCODE_ARCH" == "armhf" ]; then TRIPLE="arm-rpi-linux-gnueabihf" fi - export VSCODE_SYSROOT_DIR=$(Build.SourcesDirectory)/.build/sysroots + export VSCODE_SYSROOT_DIR=$(Build.SourcesDirectory)/.build/sysroots/glibc-2.28-gcc-10.5.0 export STRIP="$VSCODE_SYSROOT_DIR/$TRIPLE/$TRIPLE/bin/strip" npm run gulp "vscode-linux-$(VSCODE_ARCH)-prepare-rpm" env: diff --git a/build/azure-pipelines/linux/setup-env.sh b/build/azure-pipelines/linux/setup-env.sh index 94105642b19..a148d519fe6 100755 --- a/build/azure-pipelines/linux/setup-env.sh +++ b/build/azure-pipelines/linux/setup-env.sh @@ -7,17 +7,25 @@ if [ "$SYSROOT_ARCH" == "x64" ]; then SYSROOT_ARCH="amd64" fi -export VSCODE_SYSROOT_DIR=$PWD/.build/sysroots -if [ -d "$VSCODE_SYSROOT_DIR" ]; then - echo "Using cached sysroot" +export VSCODE_CLIENT_SYSROOT_DIR=$PWD/.build/sysroots/glibc-2.28-gcc-10.5.0 +export VSCODE_REMOTE_SYSROOT_DIR=$PWD/.build/sysroots/glibc-2.28-gcc-8.5.0 +if [ -d "$VSCODE_CLIENT_SYSROOT_DIR" ]; then + echo "Using cached client sysroot" else - echo "Downloading sysroot" - SYSROOT_ARCH="$SYSROOT_ARCH" node -e '(async () => { const { getVSCodeSysroot } = require("./build/linux/debian/install-sysroot.js"); await getVSCodeSysroot(process.env["SYSROOT_ARCH"]); })()' + echo "Downloading client sysroot" + SYSROOT_ARCH="$SYSROOT_ARCH" VSCODE_SYSROOT_DIR="$VSCODE_CLIENT_SYSROOT_DIR" node -e '(async () => { const { getVSCodeSysroot } = require("./build/linux/debian/install-sysroot.js"); await getVSCodeSysroot(process.env["SYSROOT_ARCH"]); })()' +fi + +if [ -d "$VSCODE_REMOTE_SYSROOT_DIR" ]; then + echo "Using cached remote sysroot" +else + echo "Downloading remote sysroot" + SYSROOT_ARCH="$SYSROOT_ARCH" VSCODE_SYSROOT_DIR="$VSCODE_REMOTE_SYSROOT_DIR" VSCODE_SYSROOT_PREFIX="-glibc-2.28-gcc-8.5.0" node -e '(async () => { const { getVSCodeSysroot } = require("./build/linux/debian/install-sysroot.js"); await getVSCodeSysroot(process.env["SYSROOT_ARCH"]); })()' fi if [ "$npm_config_arch" == "x64" ]; then # Download clang based on chromium revision used by vscode - curl -s https://raw.githubusercontent.com/chromium/chromium/132.0.6834.210/tools/clang/scripts/update.py | python - --output-dir=$PWD/.build/CR_Clang --host-os=linux + curl -s https://raw.githubusercontent.com/chromium/chromium/134.0.6998.205/tools/clang/scripts/update.py | python - --output-dir=$PWD/.build/CR_Clang --host-os=linux # Download libcxx headers and objects from upstream electron releases DEBUG=libcxx-fetcher \ @@ -29,41 +37,41 @@ if [ "$npm_config_arch" == "x64" ]; then # Set compiler toolchain # Flags for the client build are based on - # https://source.chromium.org/chromium/chromium/src/+/refs/tags/132.0.6834.210:build/config/arm.gni - # https://source.chromium.org/chromium/chromium/src/+/refs/tags/132.0.6834.210:build/config/compiler/BUILD.gn - # https://source.chromium.org/chromium/chromium/src/+/refs/tags/132.0.6834.210:build/config/c++/BUILD.gn - export CC="$PWD/.build/CR_Clang/bin/clang --gcc-toolchain=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu" - export CXX="$PWD/.build/CR_Clang/bin/clang++ --gcc-toolchain=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu" - export CXXFLAGS="-nostdinc++ -D__NO_INLINE__ -DSPDLOG_USE_STD_FORMAT -I$PWD/.build/libcxx_headers -isystem$PWD/.build/libcxx_headers/include -isystem$PWD/.build/libcxxabi_headers/include -fPIC -flto=thin -fsplit-lto-unit -D_LIBCPP_ABI_NAMESPACE=Cr -D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_EXTENSIVE --sysroot=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot" - export LDFLAGS="-stdlib=libc++ --sysroot=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot -fuse-ld=lld -flto=thin -L$PWD/.build/libcxx-objects -lc++abi -L$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot/usr/lib/x86_64-linux-gnu -L$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot/lib/x86_64-linux-gnu -Wl,--lto-O0" + # https://source.chromium.org/chromium/chromium/src/+/refs/tags/134.0.6998.205:build/config/arm.gni + # https://source.chromium.org/chromium/chromium/src/+/refs/tags/134.0.6998.205:build/config/compiler/BUILD.gn + # https://source.chromium.org/chromium/chromium/src/+/refs/tags/134.0.6998.205:build/config/c++/BUILD.gn + export CC="$PWD/.build/CR_Clang/bin/clang --gcc-toolchain=$VSCODE_CLIENT_SYSROOT_DIR/x86_64-linux-gnu" + export CXX="$PWD/.build/CR_Clang/bin/clang++ --gcc-toolchain=$VSCODE_CLIENT_SYSROOT_DIR/x86_64-linux-gnu" + export CXXFLAGS="-nostdinc++ -D__NO_INLINE__ -DSPDLOG_USE_STD_FORMAT -I$PWD/.build/libcxx_headers -isystem$PWD/.build/libcxx_headers/include -isystem$PWD/.build/libcxxabi_headers/include -fPIC -flto=thin -fsplit-lto-unit -D_LIBCPP_ABI_NAMESPACE=Cr -D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_EXTENSIVE --sysroot=$VSCODE_CLIENT_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot" + export LDFLAGS="-stdlib=libc++ --sysroot=$VSCODE_CLIENT_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot -fuse-ld=lld -flto=thin -L$PWD/.build/libcxx-objects -lc++abi -L$VSCODE_CLIENT_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot/usr/lib/x86_64-linux-gnu -L$VSCODE_CLIENT_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot/lib/x86_64-linux-gnu -Wl,--lto-O0" # Set compiler toolchain for remote server - export VSCODE_REMOTE_CC=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/bin/x86_64-linux-gnu-gcc - export VSCODE_REMOTE_CXX=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/bin/x86_64-linux-gnu-g++ - export VSCODE_REMOTE_CXXFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot" - export VSCODE_REMOTE_LDFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot -L$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot/usr/lib/x86_64-linux-gnu -L$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot/lib/x86_64-linux-gnu" + export VSCODE_REMOTE_CC=$VSCODE_REMOTE_SYSROOT_DIR/x86_64-linux-gnu/bin/x86_64-linux-gnu-gcc + export VSCODE_REMOTE_CXX=$VSCODE_REMOTE_SYSROOT_DIR/x86_64-linux-gnu/bin/x86_64-linux-gnu-g++ + export VSCODE_REMOTE_CXXFLAGS="--sysroot=$VSCODE_REMOTE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot" + export VSCODE_REMOTE_LDFLAGS="--sysroot=$VSCODE_REMOTE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot -L$VSCODE_REMOTE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot/usr/lib/x86_64-linux-gnu -L$VSCODE_REMOTE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot/lib/x86_64-linux-gnu" elif [ "$npm_config_arch" == "arm64" ]; then # Set compiler toolchain for client native modules - export CC=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/bin/aarch64-linux-gnu-gcc - export CXX=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/bin/aarch64-linux-gnu-g++ - export CXXFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot" - export LDFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot -L$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot/usr/lib/aarch64-linux-gnu -L$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot/lib/aarch64-linux-gnu" + export CC=$VSCODE_CLIENT_SYSROOT_DIR/aarch64-linux-gnu/bin/aarch64-linux-gnu-gcc + export CXX=$VSCODE_CLIENT_SYSROOT_DIR/aarch64-linux-gnu/bin/aarch64-linux-gnu-g++ + export CXXFLAGS="--sysroot=$VSCODE_CLIENT_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot" + export LDFLAGS="--sysroot=$VSCODE_CLIENT_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot -L$VSCODE_CLIENT_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot/usr/lib/aarch64-linux-gnu -L$VSCODE_CLIENT_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot/lib/aarch64-linux-gnu" # Set compiler toolchain for remote server - export VSCODE_REMOTE_CC=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/bin/aarch64-linux-gnu-gcc - export VSCODE_REMOTE_CXX=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/bin/aarch64-linux-gnu-g++ - export VSCODE_REMOTE_CXXFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot" - export VSCODE_REMOTE_LDFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot -L$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot/usr/lib/aarch64-linux-gnu -L$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot/lib/aarch64-linux-gnu" + export VSCODE_REMOTE_CC=$VSCODE_REMOTE_SYSROOT_DIR/aarch64-linux-gnu/bin/aarch64-linux-gnu-gcc + export VSCODE_REMOTE_CXX=$VSCODE_REMOTE_SYSROOT_DIR/aarch64-linux-gnu/bin/aarch64-linux-gnu-g++ + export VSCODE_REMOTE_CXXFLAGS="--sysroot=$VSCODE_REMOTE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot" + export VSCODE_REMOTE_LDFLAGS="--sysroot=$VSCODE_REMOTE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot -L$VSCODE_REMOTE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot/usr/lib/aarch64-linux-gnu -L$VSCODE_REMOTE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot/lib/aarch64-linux-gnu" elif [ "$npm_config_arch" == "arm" ]; then # Set compiler toolchain for client native modules - export CC=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/bin/arm-rpi-linux-gnueabihf-gcc - export CXX=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/bin/arm-rpi-linux-gnueabihf-g++ - export CXXFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot" - export LDFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot -L$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot/usr/lib/arm-linux-gnueabihf -L$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot/lib/arm-linux-gnueabihf" + export CC=$VSCODE_CLIENT_SYSROOT_DIR/arm-rpi-linux-gnueabihf/bin/arm-rpi-linux-gnueabihf-gcc + export CXX=$VSCODE_CLIENT_SYSROOT_DIR/arm-rpi-linux-gnueabihf/bin/arm-rpi-linux-gnueabihf-g++ + export CXXFLAGS="--sysroot=$VSCODE_CLIENT_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot" + export LDFLAGS="--sysroot=$VSCODE_CLIENT_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot -L$VSCODE_CLIENT_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot/usr/lib/arm-linux-gnueabihf -L$VSCODE_CLIENT_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot/lib/arm-linux-gnueabihf" # Set compiler toolchain for remote server - export VSCODE_REMOTE_CC=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/bin/arm-rpi-linux-gnueabihf-gcc - export VSCODE_REMOTE_CXX=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/bin/arm-rpi-linux-gnueabihf-g++ - export VSCODE_REMOTE_CXXFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot" - export VSCODE_REMOTE_LDFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot -L$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot/usr/lib/arm-linux-gnueabihf -L$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot/lib/arm-linux-gnueabihf" + export VSCODE_REMOTE_CC=$VSCODE_REMOTE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/bin/arm-rpi-linux-gnueabihf-gcc + export VSCODE_REMOTE_CXX=$VSCODE_REMOTE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/bin/arm-rpi-linux-gnueabihf-g++ + export VSCODE_REMOTE_CXXFLAGS="--sysroot=$VSCODE_REMOTE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot" + export VSCODE_REMOTE_LDFLAGS="--sysroot=$VSCODE_REMOTE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot -L$VSCODE_REMOTE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot/usr/lib/arm-linux-gnueabihf -L$VSCODE_REMOTE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot/lib/arm-linux-gnueabihf" fi diff --git a/build/azure-pipelines/linux/verify-glibc-requirements.sh b/build/azure-pipelines/linux/verify-glibc-requirements.sh index c655ce74c7e..529417761f9 100755 --- a/build/azure-pipelines/linux/verify-glibc-requirements.sh +++ b/build/azure-pipelines/linux/verify-glibc-requirements.sh @@ -31,7 +31,7 @@ for file in $files; do glibcxx_version=$version fi fi - done < <("$PWD/.build/sysroots/$TRIPLE/$TRIPLE/bin/objdump" -T "$file") + done < <("$VSCODE_SYSROOT_DIR/$TRIPLE/$TRIPLE/bin/objdump" -T "$file") if [[ "$glibc_version" != "$EXPECTED_GLIBC_VERSION" ]]; then echo "Error: File $file has dependency on GLIBC > $EXPECTED_GLIBC_VERSION, found $glibc_version" @@ -39,6 +39,5 @@ for file in $files; do fi if [[ "$glibcxx_version" != "$EXPECTED_GLIBCXX_VERSION" ]]; then echo "Error: File $file has dependency on GLIBCXX > $EXPECTED_GLIBCXX_VERSION, found $glibcxx_version" - exit 1 fi done diff --git a/build/azure-pipelines/product-compile.yml b/build/azure-pipelines/product-compile.yml index fba31eefcd1..a69942b9d0c 100644 --- a/build/azure-pipelines/product-compile.yml +++ b/build/azure-pipelines/product-compile.yml @@ -104,12 +104,12 @@ steps: - template: common/install-builtin-extensions.yml@self - ${{ if eq(parameters.VSCODE_QUALITY, 'oss') }}: - - script: npm exec -- npm-run-all -lp core-ci-pr extensions-ci-pr hygiene eslint valid-layers-check property-init-order-check vscode-dts-compile-check tsec-compile-check + - script: npm exec -- npm-run-all -lp core-ci-pr extensions-ci-pr hygiene eslint valid-layers-check define-class-fields-check vscode-dts-compile-check tsec-compile-check env: GITHUB_TOKEN: "$(github-distro-mixin-password)" displayName: Compile & Hygiene (OSS) - ${{ else }}: - - script: npm exec -- npm-run-all -lp core-ci extensions-ci hygiene eslint valid-layers-check property-init-order-check vscode-dts-compile-check tsec-compile-check + - script: npm exec -- npm-run-all -lp core-ci extensions-ci hygiene eslint valid-layers-check define-class-fields-check vscode-dts-compile-check tsec-compile-check env: GITHUB_TOKEN: "$(github-distro-mixin-password)" displayName: Compile & Hygiene (non-OSS) diff --git a/build/azure-pipelines/win32/product-build-win32-test.yml b/build/azure-pipelines/win32/product-build-win32-test.yml index 8af78682146..571e877947c 100644 --- a/build/azure-pipelines/win32/product-build-win32-test.yml +++ b/build/azure-pipelines/win32/product-build-win32-test.yml @@ -16,7 +16,8 @@ parameters: default: PublishPipelineArtifact@0 steps: - - powershell: npm exec -- npm-run-all -lp "electron $(VSCODE_ARCH)" "playwright-install" + # Additional "--" needed to workaround https://github.com/npm/cli/issues/7375 + - powershell: npm exec -- -- npm-run-all -lp "electron $(VSCODE_ARCH)" "playwright-install" env: GITHUB_TOKEN: "$(github-distro-mixin-password)" displayName: Download Electron and Playwright @@ -41,12 +42,14 @@ steps: - powershell: .\scripts\test.bat --build --tfs "Unit Tests" displayName: 🧪 Run unit tests (Electron) timeoutInMinutes: 15 - - powershell: npm run test-node -- --build + # Additional "--" needed to workaround https://github.com/npm/cli/issues/7375 + - powershell: npm run test-node -- -- --build displayName: 🧪 Run unit tests (node.js) timeoutInMinutes: 15 - ${{ if eq(parameters.VSCODE_RUN_BROWSER_TESTS, true) }}: - - powershell: npm run test-browser-no-install -- --build --browser chromium --tfs "Browser Unit Tests" + # Additional "--" needed to workaround https://github.com/npm/cli/issues/7375 + - powershell: npm run test-browser-no-install -- -- --build --browser chromium --tfs "Browser Unit Tests" displayName: 🧪 Run unit tests (Browser, Chromium) timeoutInMinutes: 20 @@ -153,25 +156,29 @@ steps: # displayName: Build extensions for smoke tests # - ${{ if eq(parameters.VSCODE_RUN_ELECTRON_TESTS, true) }}: - # - powershell: npm run smoketest-no-compile -- --tracing + # # Additional "--" needed to workaround https://github.com/npm/cli/issues/7375 + # - powershell: npm run smoketest-no-compile -- -- --tracing # displayName: 🧪 Run smoke tests (Electron) # timeoutInMinutes: 20 - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: - ${{ if eq(parameters.VSCODE_RUN_ELECTRON_TESTS, true) }}: - - powershell: npm run smoketest-no-compile -- --tracing --build "$(agent.builddirectory)\VSCode-win32-$(VSCODE_ARCH)" + # Additional "--" needed to workaround https://github.com/npm/cli/issues/7375 + - powershell: npm run smoketest-no-compile -- -- --tracing --build "$(agent.builddirectory)\VSCode-win32-$(VSCODE_ARCH)" displayName: 🧪 Run smoke tests (Electron) timeoutInMinutes: 20 - ${{ if eq(parameters.VSCODE_RUN_BROWSER_TESTS, true) }}: - - powershell: npm run smoketest-no-compile -- --web --tracing --headless + # Additional "--" needed to workaround https://github.com/npm/cli/issues/7375 + - powershell: npm run smoketest-no-compile -- -- --web --tracing --headless env: VSCODE_REMOTE_SERVER_PATH: $(agent.builddirectory)\vscode-server-win32-$(VSCODE_ARCH)-web displayName: 🧪 Run smoke tests (Browser, Chromium) timeoutInMinutes: 20 - ${{ if eq(parameters.VSCODE_RUN_REMOTE_TESTS, true) }}: - - powershell: npm run smoketest-no-compile -- --tracing --remote --build "$(agent.builddirectory)\VSCode-win32-$(VSCODE_ARCH)" + # Additional "--" needed to workaround https://github.com/npm/cli/issues/7375 + - powershell: npm run smoketest-no-compile -- -- --tracing --remote --build "$(agent.builddirectory)\VSCode-win32-$(VSCODE_ARCH)" env: VSCODE_REMOTE_SERVER_PATH: $(agent.builddirectory)\vscode-server-win32-$(VSCODE_ARCH) displayName: 🧪 Run smoke tests (Remote) diff --git a/build/azure-pipelines/win32/product-build-win32.yml b/build/azure-pipelines/win32/product-build-win32.yml index e561d8e09b1..92e60170ba4 100644 --- a/build/azure-pipelines/win32/product-build-win32.yml +++ b/build/azure-pipelines/win32/product-build-win32.yml @@ -221,10 +221,11 @@ steps: echo "##vso[task.setvariable variable=EsrpCliDllPath]$Version\net6.0\esrpcli.dll" displayName: Find ESRP CLI + # Additional "--" needed to workaround https://github.com/npm/cli/issues/7375 - powershell: | . build/azure-pipelines/win32/exec.ps1 $ErrorActionPreference = "Stop" - exec { npx deemon --detach --wait -- npx zx build/azure-pipelines/win32/codesign.js } + exec { npx deemon --detach --wait -- -- npx zx build/azure-pipelines/win32/codesign.js } env: SYSTEM_ACCESSTOKEN: $(System.AccessToken) displayName: ✍️ Codesign @@ -243,10 +244,11 @@ steps: - ${{ if ne(parameters.VSCODE_CIBUILD, true) }}: - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: + # Additional "--" needed to workaround https://github.com/npm/cli/issues/7375 - powershell: | . build/azure-pipelines/win32/exec.ps1 $ErrorActionPreference = "Stop" - exec { npx deemon --attach -- npx zx build/azure-pipelines/win32/codesign.js } + exec { npx deemon --attach -- -- npx zx build/azure-pipelines/win32/codesign.js } condition: succeededOrFailed() displayName: "✍️ Post-job: Codesign" diff --git a/build/checksums/electron.txt b/build/checksums/electron.txt index 9bfbc95a4c8..48bd3d92695 100644 --- a/build/checksums/electron.txt +++ b/build/checksums/electron.txt @@ -1,75 +1,75 @@ -249f89d35cb6bd74edc07551b141bffc2045847c4cf9e57e21089d5082bdb4b4 *chromedriver-v34.5.1-darwin-arm64.zip -7ff68fd26f225deaa8c6fbcd76dc80a00f9ef73f9118075f3e2ab54dfb0c810e *chromedriver-v34.5.1-darwin-x64.zip -749f692603527e8743c81d05eb2de2e281e2b03b148ec00379f13e8da17ca7a4 *chromedriver-v34.5.1-linux-arm64.zip -14bcc062457cf31d606451aa7fae1baae720a944dead06231fe2a55f17d39966 *chromedriver-v34.5.1-linux-armv7l.zip -57cf85eb9dafe28ccdd8ba4a095cb1fd5b8c71f0743bf532b132bc45e56630ef *chromedriver-v34.5.1-linux-x64.zip -e90e10cf45f4aaba1d8b763279b7c4b85e1132bdc9faef834ffda41ee1460df8 *chromedriver-v34.5.1-mas-arm64.zip -1206e1c71ec0360be9531e48c0292ffac37e40d8d7a48dd38f1108d3d3ccc0c0 *chromedriver-v34.5.1-mas-x64.zip -1b226994cfa02663f23edfb0c8a4d3e218b7c4d037a90bbb4800a7c396b67d9f *chromedriver-v34.5.1-win32-arm64.zip -dc38291ccad6f715a82cc2ce0cfffe3bb37612fa86013d405e878ea74e4c5fb8 *chromedriver-v34.5.1-win32-ia32.zip -3ccc7e4b65adde12e26b7affeea30b9597b8841fc2a4d3c50c042e80b85853ac *chromedriver-v34.5.1-win32-x64.zip -71fe75d29208ca9e38754d903af4d5d6e80c62b04097605c36ebf722c2447842 *electron-api.json -009c833bd014b6f873974c5d3189905e705ebcb188a90ae05b60ea252319a46e *electron-v34.5.1-darwin-arm64-dsym-snapshot.zip -c5f5722c55e75e9860cb203e03626c04f30f272ef17b735946fd723600ee07ea *electron-v34.5.1-darwin-arm64-dsym.zip -06de49512ac4b0b4e374bdcd296e8c70584fb47207bb6caed9122e3cef5da8f7 *electron-v34.5.1-darwin-arm64-symbols.zip -78411442b5bd2252cf4605b6a44c35ad6a06807d03c63c61726ad7693c6d5893 *electron-v34.5.1-darwin-arm64.zip -e90b292974251336ae8990a74977065ac4dd6388836ccd1cfee3a1599a37bd39 *electron-v34.5.1-darwin-x64-dsym-snapshot.zip -35a0ac52f6036cd0a7d4bc9848477b653095b210497e36797427ff8fe3194c7f *electron-v34.5.1-darwin-x64-dsym.zip -0457bb7413c770245912342a6dd07c3588f586e8d868e0dd534179e22b07898c *electron-v34.5.1-darwin-x64-symbols.zip -8d4bc5f4495ef952589891b6c70a86d8a9d143a1d4d90d15dd81926639822031 *electron-v34.5.1-darwin-x64.zip -73be60acd1f3773f87b283eef8c26e257f16efd46a179c143311b1b9fcb4a61a *electron-v34.5.1-linux-arm64-debug.zip -53677a8f437b36b79481eb6c6f9f7557606af04ef94cce751620e8206dad36a8 *electron-v34.5.1-linux-arm64-symbols.zip -4c0d5833faa01cc3a586087b82f719c2fe331515d26bb3fa098dc79bd3ea153f *electron-v34.5.1-linux-arm64.zip -73be60acd1f3773f87b283eef8c26e257f16efd46a179c143311b1b9fcb4a61a *electron-v34.5.1-linux-armv7l-debug.zip -6eb39e79bd52f566d18a1140242c7484b89d7cd77573b92fc2e2993b51d6fbf1 *electron-v34.5.1-linux-armv7l-symbols.zip -7ed517eeaff56960a01fe53fc445e4118135eeb8267d61c37ef9df943dcc35fb *electron-v34.5.1-linux-armv7l.zip -582a2206cf1e09baa8511ca21b697cc49fddd76ef7723406b449b130b3d21730 *electron-v34.5.1-linux-x64-debug.zip -7b5d60f3d6c4ef84b0855148f14295624527cc27ab395bf54640a06eb3f7a5b0 *electron-v34.5.1-linux-x64-symbols.zip -3ae6f75fa08f5c1bdb7bbcec4dc9cf7d7f53ffcf6a4292e4a482b2ce515505e7 *electron-v34.5.1-linux-x64.zip -e6ff5c411167c0cf8c82cd737f8d0c863f4371e8e1fe213d04b502584411d239 *electron-v34.5.1-mas-arm64-dsym-snapshot.zip -8d1cb700f23d8ac7ec078d4d5d07018dfae594346e7bc5652356a5fe242a2b44 *electron-v34.5.1-mas-arm64-dsym.zip -3b74614ef81382e63f189aceb87f6c3830a23ffed046d06f672d0c1a1b361e96 *electron-v34.5.1-mas-arm64-symbols.zip -eabc29959b914f623f5f2e4011cb4e35182ed9528dc30664e59ca37c806c1d7d *electron-v34.5.1-mas-arm64.zip -ee3de3f5a96efb0197022557ec2de36d92d7423426636577864b1ae744053dea *electron-v34.5.1-mas-x64-dsym-snapshot.zip -a3db9cc489720701e3f35d2f7425c97e24f74fdb78a38bc0950b68b3f82aebb2 *electron-v34.5.1-mas-x64-dsym.zip -a9131003b1ac4a3c3327ff405e1cd8f3e61dc8a73cfae3e05cb5eb0f2d872bee *electron-v34.5.1-mas-x64-symbols.zip -1b44d42dbe9cb6bc5c2fb77f708d639e01f8ec6f74b95710fc6c8dbd70181f3b *electron-v34.5.1-mas-x64.zip -4495d8bf4d3dbb5ebc3ad135f4658e09d706368d002af9f24d236e1a0a28e994 *electron-v34.5.1-win32-arm64-pdb.zip -2c31fa61d24e736f3e327eba4d354c09471fba5aa277e215f7e2ea275b323a80 *electron-v34.5.1-win32-arm64-symbols.zip -c23f84aabb09c24cd2ae759a547fdba4206af19a3bb0f4554a91cd9528648ad0 *electron-v34.5.1-win32-arm64-toolchain-profile.zip -c0cff1c83094a430f1b202bb5035b51ebcefa54cd53d798bb63de9cb96abf223 *electron-v34.5.1-win32-arm64.zip -d662fb7afc288aa15d929fecbb391c7067448ea86b4bf01e941fa8da744a8167 *electron-v34.5.1-win32-ia32-pdb.zip -2cd1f41a3297fc271e426bd0cc5f8c3474f73438a7a303186701cb7d8b26bdb6 *electron-v34.5.1-win32-ia32-symbols.zip -c23f84aabb09c24cd2ae759a547fdba4206af19a3bb0f4554a91cd9528648ad0 *electron-v34.5.1-win32-ia32-toolchain-profile.zip -cf86edf6cdb47d5cfb00c4eb68f7a18d70bf9e33f1f6a0481d51673cf6af7050 *electron-v34.5.1-win32-ia32.zip -9dd0e6f6ef53f8bd4d7ecd97a3bfc7e8a98de8771986071692afc57d57d199d6 *electron-v34.5.1-win32-x64-pdb.zip -f50ab96420bddd43bd5dbd56130cfcd69eea2dba18bfd3c8c3b4bb189bb033e6 *electron-v34.5.1-win32-x64-symbols.zip -c23f84aabb09c24cd2ae759a547fdba4206af19a3bb0f4554a91cd9528648ad0 *electron-v34.5.1-win32-x64-toolchain-profile.zip -da606d1a085a52ddf5592110b58284fc3bf49f273f6f2e7d6a8341c98af8498e *electron-v34.5.1-win32-x64.zip -793ae7822cbdad6270c318f3c93c0e8b4f9276dea6dd87db2d1297cadc7381c6 *electron.d.ts -1d1465b4f6a3919a6e8e2fb8612b29f61b09d80f386a8fa2b806859b9be0d0bf *ffmpeg-v34.5.1-darwin-arm64.zip -e138b6422dd1648cbe817b99f59476c65ed9946d50e50094124eae660b416378 *ffmpeg-v34.5.1-darwin-x64.zip -346101611df565cabcfaa3515b1db3f70d0891ba8f1241074dd09b69e12630d2 *ffmpeg-v34.5.1-linux-arm64.zip -5c77c712ee93bd26706daa78f0651d9b4ba8e4b46a115908f29d2742a2e1b9f0 *ffmpeg-v34.5.1-linux-armv7l.zip -f5ab70d399d528450c9499966e88ce02a368bb8c7dd7ac0676a6628fa29b3f14 *ffmpeg-v34.5.1-linux-x64.zip -10e3424c01b946274fa8c651d4ea79032637feca4c8712ebb1c00f392711594f *ffmpeg-v34.5.1-mas-arm64.zip -4db0373915c2c2a055bd04755acdbcd08e00456f1fb92fefc0e05cd7fb48e4fa *ffmpeg-v34.5.1-mas-x64.zip -c8cca82fc9315f86ffb60b39e824ebec7f98361f8773ea0618d9feea92b88412 *ffmpeg-v34.5.1-win32-arm64.zip -c8cca82fc9315f86ffb60b39e824ebec7f98361f8773ea0618d9feea92b88412 *ffmpeg-v34.5.1-win32-ia32.zip -c8cca82fc9315f86ffb60b39e824ebec7f98361f8773ea0618d9feea92b88412 *ffmpeg-v34.5.1-win32-x64.zip -9ae3a56bf29d9704cd8cf32924aad89414f28d439e61dd54bdd8b4259b8d0b1d *hunspell_dictionaries.zip -691e23913b7dbde1f9c9b6e9f13f06353d5c7927cbab6d48b7de43e76e5eacd8 *libcxx-objects-v34.5.1-linux-arm64.zip -eaaa18779a96873daeece21c7c823d1f5d4759f8eca79dbbbf2055635df6112f *libcxx-objects-v34.5.1-linux-armv7l.zip -6a2e3dfcea9d0582ecbc2a6be124f0e830e2194111bd9aa6a9843cb956c946c4 *libcxx-objects-v34.5.1-linux-x64.zip -d4b70d94523ebd770009dba04c842450539a9bdc856de660a7391620d3bcc1fb *libcxx_headers.zip -0ed01bc1908fd8f7519ccdf636b5732c6fe2c095a6dc35a13eb6c79b9e87d7d1 *libcxxabi_headers.zip -f633cd0df0b08a15938ccdc77480bc28eb96fd85936ef76c343cc3f47fe74f3c *mksnapshot-v34.5.1-darwin-arm64.zip -a8643285a2386960ceb608ff34d6dac33942142e821e2e0c670b282389a87e53 *mksnapshot-v34.5.1-darwin-x64.zip -70863b79d4b7ab75d013a9192f7b23165e3e523b243632c7b55418767527e022 *mksnapshot-v34.5.1-linux-arm64-x64.zip -c30319434ea16416c38bbdf847432fd37fd8e1aa78c1c22b6345d02e3743c016 *mksnapshot-v34.5.1-linux-armv7l-x64.zip -e882e32b67501d36710da396167274158c1940afe67459ffa1d9df534a8f6df1 *mksnapshot-v34.5.1-linux-x64.zip -af1d08dbd3c572ae10db0d24203a28d83c87e92e966064134ec5d7770c74e3ac *mksnapshot-v34.5.1-mas-arm64.zip -238058875abebcb9233e609fadad76e85b79530f1bdfb60498b144fec82ff8fc *mksnapshot-v34.5.1-mas-x64.zip -779e494cf2088ee386bb3ffd68d5efc2de3d43e5a2e6a5a768638799c460fdab *mksnapshot-v34.5.1-win32-arm64-x64.zip -9f9790fab86209ca76ecfae3e20dc028bc0e49574872f6ac17b8856093811357 *mksnapshot-v34.5.1-win32-ia32.zip -5c39077fd59426108f15e4981c7be5ebe56aa706b9d166853225de882fee8d6e *mksnapshot-v34.5.1-win32-x64.zip +7c5a78d80e17950a9867f4c07d8c10a387d537cf4a08c7349ce2df42e527f594 *chromedriver-v35.2.2-darwin-arm64.zip +88b4f789f98205f678faa75fdfadd41a3658e3c5f0567e7d619fd80b14f24aaf *chromedriver-v35.2.2-darwin-x64.zip +63ae8e84ba403acb21027c891074f6bc53502875b887c60a0574c514dc35ec55 *chromedriver-v35.2.2-linux-arm64.zip +6fc034bf391cbf8df69063c49311b9b043f1157cbc4a2ba40a49607637fe370d *chromedriver-v35.2.2-linux-armv7l.zip +8d676d70013be97a45842f5c9668ad1cf7736b71cc16a3d1f569a6b4975191f7 *chromedriver-v35.2.2-linux-x64.zip +5b159a5b9c83a37f5218902f6537aa1468c16dd0b86c501f338146310f26ac6b *chromedriver-v35.2.2-mas-arm64.zip +397543891529df3186cd8d3dd528dd37cae4f6d6df996cb69e4b233ad3480e28 *chromedriver-v35.2.2-mas-x64.zip +bdc6e2cc8b427d50b9162bf9a8c22bfd9c418825b99c56497a60a7a36f2be860 *chromedriver-v35.2.2-win32-arm64.zip +d999814eba0eeca76b1f3b183d833cb019a5c7cb7dfba79956eff5ea199e7e03 *chromedriver-v35.2.2-win32-ia32.zip +e09d7a0d5f1a069e1b9804d2f4ea41d8daac56b7a7201e9a62156714b2c2a2ed *chromedriver-v35.2.2-win32-x64.zip +f65be61ebb484d32277efe1b543fc4a4145a76c198492dd713982b61f382b782 *electron-api.json +8aad9f503464bcb670dc2eade4bd612e596f151d8e6f7611b5e33d6a29061a6b *electron-v35.2.2-darwin-arm64-dsym-snapshot.zip +36813a4132b27d29bd5189bb60c66101885ad356df5753df65fde867915da62f *electron-v35.2.2-darwin-arm64-dsym.zip +004a1b7c92ea3541298997b0b03533d74b7a36953f12a8a7e6d0bce6725bcebf *electron-v35.2.2-darwin-arm64-symbols.zip +ed8692c0c4bfbf4462e5ddbf0843a049026ef2c1ca330c75ae1f6ad15c6539c9 *electron-v35.2.2-darwin-arm64.zip +8fdf826608c8cd96633e93efc5080b09907934f0a68fdc70ea69f12dc4547217 *electron-v35.2.2-darwin-x64-dsym-snapshot.zip +cfde0f4cf49666c08eea793c31933a569ebc594e35c192791bddfebeea5cc331 *electron-v35.2.2-darwin-x64-dsym.zip +5b13caab9a012750a2ea38aecd969dd069ea7c8e4c2c0eb4831124abc05872e5 *electron-v35.2.2-darwin-x64-symbols.zip +4139a996cb7c38a564c8eadd098f4ffee3f46b86d84e6c9db940fadd1ed2b577 *electron-v35.2.2-darwin-x64.zip +7af854d0ac1c49356abae293f5a9604a53c64a60c5e00d47ea5d2fb537c3bbbd *electron-v35.2.2-linux-arm64-debug.zip +17e5a66778922574db81b2bda54582c213ab8e434bf75283fedebfe74fe681d1 *electron-v35.2.2-linux-arm64-symbols.zip +da40ba88ce4f71649200d887a796ec225f22f34caddb7d81f2649ce864e36f65 *electron-v35.2.2-linux-arm64.zip +7af854d0ac1c49356abae293f5a9604a53c64a60c5e00d47ea5d2fb537c3bbbd *electron-v35.2.2-linux-armv7l-debug.zip +9ad44dae2a0cbb3c968422e1402a7c727cd01e71d9dce1fe15701a4f49e72c5f *electron-v35.2.2-linux-armv7l-symbols.zip +43775ff1a99fef3fcf20de676abc61e825b2205ccaef40ce2677ca97b1fda0bd *electron-v35.2.2-linux-armv7l.zip +edc7adc3da98083ce172c23877b1a1df53dc1738117a59151cb21962a6d02864 *electron-v35.2.2-linux-x64-debug.zip +c011ef6cb5b67153ae779d8c3a6d2784a3738b5dbf215f4565199b38018261fe *electron-v35.2.2-linux-x64-symbols.zip +a164a20ef4e5c024fda3a35a3d57750530e3ad9633abe1f09498a24f0711457b *electron-v35.2.2-linux-x64.zip +494b86a601a472c9431bfa2280cf99c63e5803be246704f5d17fb8c2d1679172 *electron-v35.2.2-mas-arm64-dsym-snapshot.zip +2635b8465f46271de5a938c158e9d75f3beda9da0c2afd503e01ad97e113a43b *electron-v35.2.2-mas-arm64-dsym.zip +d7f2b02c975e5a04eab446bedcaae67e29b89d5fce32b84cb82de0338a84a139 *electron-v35.2.2-mas-arm64-symbols.zip +72dfd66142c7def1ab13d711d2065e34452f52dc8af4eb84d2836c74a6dc7c9a *electron-v35.2.2-mas-arm64.zip +06ed4fff89e1ca45a3e16b99a32f9e9e8f8883b50c5861e86eb3b8012410cd48 *electron-v35.2.2-mas-x64-dsym-snapshot.zip +bdf8821475a9ca2507af0994063276cb26f18c0fd5a8b153ff7ff2c902a268e3 *electron-v35.2.2-mas-x64-dsym.zip +fde15bf12d0e9a18131f9e88713538012a3da08bcec7f617beb3d37de99a3f93 *electron-v35.2.2-mas-x64-symbols.zip +c2283b48c6daf8d2c0b67a4bf044c8d6bcfe7489a1100dd2b1dbeb3d974d2b41 *electron-v35.2.2-mas-x64.zip +c5b817694ad14749f74f292237a2e7a7ff97d20cc5e42095850eb8072ebec180 *electron-v35.2.2-win32-arm64-pdb.zip +0c2992dc4161efbd89dae74d52df48fa0b81bf2af5869d6d20b1e51bff56362e *electron-v35.2.2-win32-arm64-symbols.zip +c23f84aabb09c24cd2ae759a547fdba4206af19a3bb0f4554a91cd9528648ad0 *electron-v35.2.2-win32-arm64-toolchain-profile.zip +22bfd46536e323000b1ac710950a92d0757e3d9d7944e0975627774f259b77fa *electron-v35.2.2-win32-arm64.zip +2885b1f1966e2fb13d454dea09e405dbbb63d473ef96e5cf87f1b1d932051489 *electron-v35.2.2-win32-ia32-pdb.zip +6505ee656da799aef8e3a3c4a6932bb0ff0e1d73ccd6d554becd90b7e639894c *electron-v35.2.2-win32-ia32-symbols.zip +c23f84aabb09c24cd2ae759a547fdba4206af19a3bb0f4554a91cd9528648ad0 *electron-v35.2.2-win32-ia32-toolchain-profile.zip +679b780b509594ab4485c8b5a68547d7507b16cd0706d2c06531bf6a5114a291 *electron-v35.2.2-win32-ia32.zip +41dc0d972cca8b9b14f3ad2b959be3a78fded9d8a2a3149350ecb8f484ebc412 *electron-v35.2.2-win32-x64-pdb.zip +a5d3b14fce400778a078d4096b5249cb0efe54083d9db44e1e8840a8abe99d7d *electron-v35.2.2-win32-x64-symbols.zip +c23f84aabb09c24cd2ae759a547fdba4206af19a3bb0f4554a91cd9528648ad0 *electron-v35.2.2-win32-x64-toolchain-profile.zip +045448ade76c7dbacf90c4f67c668fd28bcd7d00f60a5383f9175c1d8979a458 *electron-v35.2.2-win32-x64.zip +1154d9ef2a701d802b37b7bc285d3f736b6c2512f4cac9c1de9b520345397776 *electron.d.ts +f82f63b3c72e2ada752aaaa347c69634898693c19aacef8ffc41ee63d4763005 *ffmpeg-v35.2.2-darwin-arm64.zip +27637805014111051ed8c4690e98f15180a364ac5b2c97930c03d93592190ace *ffmpeg-v35.2.2-darwin-x64.zip +72e89440cbdd5b7eaae16f0ef5f1cb2ee43253938e07617954b8f9cd0cf6f462 *ffmpeg-v35.2.2-linux-arm64.zip +c09c0807f37170eaef01d6647ad691cd6ef8f35440c9dc42eb75c9af74e9ae93 *ffmpeg-v35.2.2-linux-armv7l.zip +c9faac57f5388d9ee280bfbd82eafab64955149eed99ee47e0e6f03316486fba *ffmpeg-v35.2.2-linux-x64.zip +f82f63b3c72e2ada752aaaa347c69634898693c19aacef8ffc41ee63d4763005 *ffmpeg-v35.2.2-mas-arm64.zip +27637805014111051ed8c4690e98f15180a364ac5b2c97930c03d93592190ace *ffmpeg-v35.2.2-mas-x64.zip +3a1ddf35788807104f5f0536dce37dac0ea4499382aa58c113ec1665c5eb2789 *ffmpeg-v35.2.2-win32-arm64.zip +3a1ddf35788807104f5f0536dce37dac0ea4499382aa58c113ec1665c5eb2789 *ffmpeg-v35.2.2-win32-ia32.zip +3a1ddf35788807104f5f0536dce37dac0ea4499382aa58c113ec1665c5eb2789 *ffmpeg-v35.2.2-win32-x64.zip +4fa74ad454d1ee0811e09df19b47d288ac932181d6c121aedb5ff07be0c146ea *hunspell_dictionaries.zip +a3d13451cddab8bc6e36b3951f8b5bc87c1e7d36ff6565d01df1cc2c2c9a349c *libcxx-objects-v35.2.2-linux-arm64.zip +9a8e1472198bfbbf8973a69b211dad09ca4390d8402920f1ec9e0327b01a0f97 *libcxx-objects-v35.2.2-linux-armv7l.zip +46e39d534b0ac84f17ea70c962549df2c500e9c28d8054bdca247b410b724780 *libcxx-objects-v35.2.2-linux-x64.zip +629a3ca35d5b49ad54f9797faec9b75df6f394d46497cb76be1318c3115c1cb2 *libcxx_headers.zip +e5c18f813cc64a7d3b0404ee9adeb9cbb49e7ee5e1054b62c71fa7d1a448ad1b *libcxxabi_headers.zip +646a9fdeec91fc7cfb71cf87a00a5d09a710ad556b5df1a98efb50099c6d4bd2 *mksnapshot-v35.2.2-darwin-arm64.zip +6debd0d795c7522f67e4b1b5b5a8db4dc1810a11d9153d98d63977d86ac641a2 *mksnapshot-v35.2.2-darwin-x64.zip +e8f296e1d56b76e4c45bf61fdadb931bcad92186516cb7bd53db7a6f3baab382 *mksnapshot-v35.2.2-linux-arm64-x64.zip +f1b01464a9dc93787885771fd5899e83c68cc90d546d190bb0319f4beef6c7f1 *mksnapshot-v35.2.2-linux-armv7l-x64.zip +09df7dde81626899a3fcb46b77713f3a86c94ea1836bb08356aaa06f3025758b *mksnapshot-v35.2.2-linux-x64.zip +f59489e81d983ecc30e812bd846e9a4c2f9c27a8082098e7e7b5111dd1418657 *mksnapshot-v35.2.2-mas-arm64.zip +5ed6fb94aaf9a2dab7c1b344f1c29c923b188862387aaacaeab1b81043faefa4 *mksnapshot-v35.2.2-mas-x64.zip +2e92f87321fac1172b214c83bfbff065a63359172ed30901de6f7370e9bad6a6 *mksnapshot-v35.2.2-win32-arm64-x64.zip +655e59bb8ddfc8620324701be08cb7b7e05165519d6230e41d641c8d89dbcf50 *mksnapshot-v35.2.2-win32-ia32.zip +9e489cba4c32c7699faae657337326b09831e3dc280c80b8477daf9d56a43ee2 *mksnapshot-v35.2.2-win32-x64.zip diff --git a/build/checksums/nodejs.txt b/build/checksums/nodejs.txt index efada69028b..57a44b439d6 100644 --- a/build/checksums/nodejs.txt +++ b/build/checksums/nodejs.txt @@ -1,7 +1,7 @@ -c016cd1975a264a29dc1b07c6fbe60d5df0a0c2beb4113c0450e3d998d1a0d9c node-v20.19.0-darwin-arm64.tar.gz -a8554af97d6491fdbdabe63d3a1cfb9571228d25a3ad9aed2df856facb131b20 node-v20.19.0-darwin-x64.tar.gz -618e4294602b78e97118a39050116b70d088b16197cd3819bba1fc18b473dfc4 node-v20.19.0-linux-arm64.tar.gz -2deb2f333b42fcdeb0d215800b3d2b9af64dd88c1d0b05e67b980398d43c4dce node-v20.19.0-linux-armv7l.tar.gz -8a4dbcdd8bccef3132d21e8543940557e55dcf44f00f0a99ba8a062f4552e722 node-v20.19.0-linux-x64.tar.gz -4ec1ae34fc7c0c65b35ec3688b9dc6d8ad5feca69d5ba45f7d72d559dc850fbb win-arm64/node.exe -6e3a39787e667d50487f7335c85636c2823a53e636d73c2c841d45da4e57906c win-x64/node.exe +e9404633bc02a5162c5c573b1e2490f5fb44648345d64a958b17e325729a5e42 node-v22.14.0-darwin-arm64.tar.gz +6698587713ab565a94a360e091df9f6d91c8fadda6d00f0cf6526e9b40bed250 node-v22.14.0-darwin-x64.tar.gz +8cf30ff7250f9463b53c18f89c6c606dfda70378215b2c905d0a9a8b08bd45e0 node-v22.14.0-linux-arm64.tar.gz +1cadf5aee7d71b6f0921235faec05e42d908ba5e8a76959f0697968fe0741204 node-v22.14.0-linux-armv7l.tar.gz +9d942932535988091034dc94cc5f42b6dc8784d6366df3a36c4c9ccb3996f0c2 node-v22.14.0-linux-x64.tar.gz +b37c6950508f266d066deb91abe2050fcd3f19e34c86ca89eed72efb40090b57 win-arm64/node.exe +33b1bc1a8aca11fd5a4f2699e51019c63c0af30cf437701d07af69be7706771b win-x64/node.exe diff --git a/build/lib/layersChecker.js b/build/lib/layersChecker.js index ab49ef0537b..012cb41b597 100644 --- a/build/lib/layersChecker.js +++ b/build/lib/layersChecker.js @@ -93,6 +93,8 @@ const CORE_TYPES = [ 'value', 'done', 'DOMException', + 'localStorage', + 'WebSocket', ]; // Types that are defined in a common layer but are known to be only // available in native environments should not be allowed in browser diff --git a/build/lib/layersChecker.ts b/build/lib/layersChecker.ts index 6c41621a1d3..f9c63eec496 100644 --- a/build/lib/layersChecker.ts +++ b/build/lib/layersChecker.ts @@ -93,6 +93,8 @@ const CORE_TYPES = [ 'value', 'done', 'DOMException', + 'localStorage', + 'WebSocket', ]; // Types that are defined in a common layer but are known to be only diff --git a/build/lib/propertyInitOrderChecker.js b/build/lib/propertyInitOrderChecker.js index c4931788047..67a17054cd6 100644 --- a/build/lib/propertyInitOrderChecker.js +++ b/build/lib/propertyInitOrderChecker.js @@ -53,78 +53,6 @@ const TS_CONFIG_PATH = path.join(__dirname, '../../', 'src', 'tsconfig.json'); // // ############################################################################################# // -const ignored = new Set([ - 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/tokenizer.ts', - 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/bracketPairsTree.ts', - 'vs/editor/common/model/textModelTokens.ts', - 'vs/editor/common/model/tokenizationTextModelPart.ts', - 'vs/editor/common/core/textEdit.ts', - 'vs/editor/browser/view/viewLayer.ts', - 'vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts', - 'vs/editor/browser/widget/diffEditor/utils.ts', - 'vs/editor/browser/observableCodeEditor.ts', - 'vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/diffEditorViewZones.ts', - 'vs/editor/browser/widget/diffEditor/diffEditorOptions.ts', - 'vs/editor/browser/widget/diffEditor/components/diffEditorEditors.ts', - 'vs/editor/browser/widget/diffEditor/features/movedBlocksLinesFeature.ts', - 'vs/editor/browser/widget/diffEditor/components/diffEditorSash.ts', - 'vs/editor/browser/widget/diffEditor/utils/editorGutter.ts', - 'vs/editor/browser/widget/diffEditor/features/gutterFeature.ts', - 'vs/editor/browser/widget/diffEditor/features/revertButtonsFeature.ts', - 'vs/editor/browser/widget/diffEditor/diffEditorWidget.ts', - 'vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts', - 'vs/editor/contrib/inlineCompletions/browser/model/suggestWidgetAdapter.ts', - 'vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts', - 'vs/editor/contrib/inlineCompletions/browser/hintsWidget/inlineCompletionsHintsWidget.ts', - 'vs/editor/contrib/inlayHints/browser/inlayHintsController.ts', - 'vs/editor/contrib/inlineCompletions/browser/model/changeRecorder.ts', - 'vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts', - 'vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditWithChanges.ts', - 'vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorView.ts', - 'vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsWordReplacementView.ts', - 'vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsLineReplacementView.ts', - 'vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsSideBySideView.ts', - 'vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/originalEditorInlineDiffView.ts', - 'vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsView.ts', - 'vs/editor/contrib/inlineCompletions/browser/view/inlineCompletionsView.ts', - 'vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts', - 'vs/editor/contrib/placeholderText/browser/placeholderTextContribution.ts', - 'vs/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter.ts', - 'vs/workbench/contrib/files/browser/views/openEditorsView.ts', - 'vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts', - 'vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts', - 'vs/workbench/contrib/chat/browser/chatInputPart.ts', - 'vs/workbench/contrib/mergeEditor/browser/model/modifiedBaseRange.ts', - 'vs/workbench/contrib/mergeEditor/browser/model/diffComputer.ts', - 'vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel.ts', - 'vs/workbench/contrib/mergeEditor/browser/view/editorGutter.ts', - 'vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts', - 'vs/workbench/contrib/mergeEditor/browser/view/editors/inputCodeEditorView.ts', - 'vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts', - 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInputModel.ts', - 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts', - 'vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel.ts', - 'vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput.ts', - 'vs/workbench/services/authentication/browser/authenticationExtensionsService.ts', - 'vs/workbench/services/textMate/browser/backgroundTokenization/textMateWorkerTokenizerController.ts', - 'vs/workbench/services/textMate/browser/textMateTokenizationFeatureImpl.ts', - 'vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts', - 'vs/workbench/contrib/notebook/browser/contrib/multicursor/notebookMulticursor.ts', - 'vs/editor/browser/widget/multiDiffEditor/diffEditorItemTemplate.ts', - 'vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.ts', - 'vs/workbench/contrib/notebook/browser/diff/notebookMultiDiffEditor.ts', - 'vs/workbench/contrib/search/common/cacheState.ts', - 'vs/workbench/contrib/multiDiffEditor/browser/scmMultiDiffSourceResolver.ts', - 'vs/workbench/contrib/mergeEditor/browser/view/viewZones.ts', - 'vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts', - 'vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts', - 'vs/workbench/contrib/accessibilitySignals/browser/editorTextPropertySignalsContribution.ts', - 'vs/workbench/contrib/inlineCompletions/browser/inlineCompletionLanguageStatusBarContribution.ts', - 'vs/workbench/contrib/welcomeDialog/browser/welcomeWidget.ts', - 'vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsWordInsertView.ts', - 'vs/workbench/api/common/extHostLanguageFeatures.ts', - 'vs/workbench/api/common/extHostSearch.ts', -]); const cancellationToken = { isCancellationRequested: () => false, throwIfCancellationRequested: () => { }, @@ -144,10 +72,6 @@ for (const file of program.getSourceFiles()) { if (!file || file.isDeclarationFile) { continue; } - const relativePath = path.relative(path.dirname(TS_CONFIG_PATH), file.fileName).replace(/\\/g, '/'); - if (ignored.has(relativePath)) { - continue; - } visit(file); } if (seenFiles.size) { diff --git a/build/lib/propertyInitOrderChecker.ts b/build/lib/propertyInitOrderChecker.ts index bbc98c6f43f..141a9c918e6 100644 --- a/build/lib/propertyInitOrderChecker.ts +++ b/build/lib/propertyInitOrderChecker.ts @@ -22,79 +22,6 @@ const TS_CONFIG_PATH = path.join(__dirname, '../../', 'src', 'tsconfig.json'); // ############################################################################################# // -const ignored = new Set([ - 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/tokenizer.ts', - 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/bracketPairsTree.ts', - 'vs/editor/common/model/textModelTokens.ts', - 'vs/editor/common/model/tokenizationTextModelPart.ts', - 'vs/editor/common/core/textEdit.ts', - 'vs/editor/browser/view/viewLayer.ts', - 'vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts', - 'vs/editor/browser/widget/diffEditor/utils.ts', - 'vs/editor/browser/observableCodeEditor.ts', - 'vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/diffEditorViewZones.ts', - 'vs/editor/browser/widget/diffEditor/diffEditorOptions.ts', - 'vs/editor/browser/widget/diffEditor/components/diffEditorEditors.ts', - 'vs/editor/browser/widget/diffEditor/features/movedBlocksLinesFeature.ts', - 'vs/editor/browser/widget/diffEditor/components/diffEditorSash.ts', - 'vs/editor/browser/widget/diffEditor/utils/editorGutter.ts', - 'vs/editor/browser/widget/diffEditor/features/gutterFeature.ts', - 'vs/editor/browser/widget/diffEditor/features/revertButtonsFeature.ts', - 'vs/editor/browser/widget/diffEditor/diffEditorWidget.ts', - 'vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts', - 'vs/editor/contrib/inlineCompletions/browser/model/suggestWidgetAdapter.ts', - 'vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts', - 'vs/editor/contrib/inlineCompletions/browser/hintsWidget/inlineCompletionsHintsWidget.ts', - 'vs/editor/contrib/inlayHints/browser/inlayHintsController.ts', - 'vs/editor/contrib/inlineCompletions/browser/model/changeRecorder.ts', - 'vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts', - 'vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditWithChanges.ts', - 'vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorView.ts', - 'vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsWordReplacementView.ts', - 'vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsLineReplacementView.ts', - 'vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsSideBySideView.ts', - 'vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/originalEditorInlineDiffView.ts', - 'vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsView.ts', - 'vs/editor/contrib/inlineCompletions/browser/view/inlineCompletionsView.ts', - 'vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts', - 'vs/editor/contrib/placeholderText/browser/placeholderTextContribution.ts', - 'vs/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter.ts', - 'vs/workbench/contrib/files/browser/views/openEditorsView.ts', - 'vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts', - 'vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts', - 'vs/workbench/contrib/chat/browser/chatInputPart.ts', - 'vs/workbench/contrib/mergeEditor/browser/model/modifiedBaseRange.ts', - 'vs/workbench/contrib/mergeEditor/browser/model/diffComputer.ts', - 'vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel.ts', - 'vs/workbench/contrib/mergeEditor/browser/view/editorGutter.ts', - 'vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts', - 'vs/workbench/contrib/mergeEditor/browser/view/editors/inputCodeEditorView.ts', - 'vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts', - 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInputModel.ts', - 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts', - 'vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel.ts', - 'vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput.ts', - 'vs/workbench/services/authentication/browser/authenticationExtensionsService.ts', - 'vs/workbench/services/textMate/browser/backgroundTokenization/textMateWorkerTokenizerController.ts', - 'vs/workbench/services/textMate/browser/textMateTokenizationFeatureImpl.ts', - 'vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts', - 'vs/workbench/contrib/notebook/browser/contrib/multicursor/notebookMulticursor.ts', - 'vs/editor/browser/widget/multiDiffEditor/diffEditorItemTemplate.ts', - 'vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.ts', - 'vs/workbench/contrib/notebook/browser/diff/notebookMultiDiffEditor.ts', - 'vs/workbench/contrib/search/common/cacheState.ts', - 'vs/workbench/contrib/multiDiffEditor/browser/scmMultiDiffSourceResolver.ts', - 'vs/workbench/contrib/mergeEditor/browser/view/viewZones.ts', - 'vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts', - 'vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts', - 'vs/workbench/contrib/accessibilitySignals/browser/editorTextPropertySignalsContribution.ts', - 'vs/workbench/contrib/inlineCompletions/browser/inlineCompletionLanguageStatusBarContribution.ts', - 'vs/workbench/contrib/welcomeDialog/browser/welcomeWidget.ts', - 'vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsWordInsertView.ts', - 'vs/workbench/api/common/extHostLanguageFeatures.ts', - 'vs/workbench/api/common/extHostSearch.ts', -]); - const cancellationToken: ts.CancellationToken = { isCancellationRequested: () => false, @@ -125,12 +52,6 @@ for (const file of program.getSourceFiles()) { if (!file || file.isDeclarationFile) { continue; } - - const relativePath = path.relative(path.dirname(TS_CONFIG_PATH), file.fileName).replace(/\\/g, '/'); - if (ignored.has(relativePath)) { - continue; - } - visit(file); } diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index e9cddd521d1..721f7b1920e 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -342,6 +342,7 @@ "--vscode-editorWidget-foreground", "--vscode-editorWidget-resizeBorder", "--vscode-errorForeground", + "--vscode-extension-border", "--vscode-extensionBadge-remoteBackground", "--vscode-extensionBadge-remoteForeground", "--vscode-extensionButton-background", @@ -956,4 +957,4 @@ "--monaco-editor-warning-decoration", "--animation-opacity" ] -} \ No newline at end of file +} diff --git a/build/linux/debian/dep-lists.js b/build/linux/debian/dep-lists.js index 8ac57b94d7f..4ef448d454e 100644 --- a/build/linux/debian/dep-lists.js +++ b/build/linux/debian/dep-lists.js @@ -86,6 +86,7 @@ exports.referenceGeneratedDepsByArch = { 'libstdc++6 (>= 5)', 'libstdc++6 (>= 5.2)', 'libstdc++6 (>= 6)', + 'libstdc++6 (>= 9)', 'libudev1 (>= 183)', 'libx11-6', 'libx11-6 (>= 2:1.4.99.1)', @@ -124,6 +125,7 @@ exports.referenceGeneratedDepsByArch = { 'libstdc++6 (>= 5)', 'libstdc++6 (>= 5.2)', 'libstdc++6 (>= 6)', + 'libstdc++6 (>= 9)', 'libudev1 (>= 183)', 'libx11-6', 'libx11-6 (>= 2:1.4.99.1)', diff --git a/build/linux/debian/dep-lists.ts b/build/linux/debian/dep-lists.ts index df119e8b485..5b7ccd51e09 100644 --- a/build/linux/debian/dep-lists.ts +++ b/build/linux/debian/dep-lists.ts @@ -86,6 +86,7 @@ export const referenceGeneratedDepsByArch = { 'libstdc++6 (>= 5)', 'libstdc++6 (>= 5.2)', 'libstdc++6 (>= 6)', + 'libstdc++6 (>= 9)', 'libudev1 (>= 183)', 'libx11-6', 'libx11-6 (>= 2:1.4.99.1)', @@ -124,6 +125,7 @@ export const referenceGeneratedDepsByArch = { 'libstdc++6 (>= 5)', 'libstdc++6 (>= 5.2)', 'libstdc++6 (>= 6)', + 'libstdc++6 (>= 9)', 'libudev1 (>= 183)', 'libx11-6', 'libx11-6 (>= 2:1.4.99.1)', diff --git a/build/linux/debian/install-sysroot.js b/build/linux/debian/install-sysroot.js index 230fbda4de6..612d0a37fb0 100644 --- a/build/linux/debian/install-sysroot.js +++ b/build/linux/debian/install-sysroot.js @@ -122,7 +122,7 @@ async function fetchUrl(options, retries = 10, retryDelay = 1000) { async function getVSCodeSysroot(arch, isMusl = false) { let expectedName; let triple; - const prefix = process.env['VSCODE_SYSROOT_PREFIX'] ?? '-glibc-2.28-gcc-8.5.0'; + const prefix = process.env['VSCODE_SYSROOT_PREFIX'] ?? '-glibc-2.28-gcc-10.5.0'; switch (arch) { case 'amd64': expectedName = `x86_64-linux-gnu${prefix}.tar.gz`; @@ -159,7 +159,7 @@ async function getVSCodeSysroot(arch, isMusl = false) { } console.log(`Installing ${arch} root image: ${sysroot}`); fs_1.default.rmSync(sysroot, { recursive: true, force: true }); - fs_1.default.mkdirSync(sysroot); + fs_1.default.mkdirSync(sysroot, { recursive: true }); await fetchUrl({ checksumSha256, assetName: expectedName, diff --git a/build/linux/debian/install-sysroot.ts b/build/linux/debian/install-sysroot.ts index 23cce9e1002..8a611593b62 100644 --- a/build/linux/debian/install-sysroot.ts +++ b/build/linux/debian/install-sysroot.ts @@ -136,7 +136,7 @@ type SysrootDictEntry = { export async function getVSCodeSysroot(arch: DebianArchString, isMusl: boolean = false): Promise { let expectedName: string; let triple: string; - const prefix = process.env['VSCODE_SYSROOT_PREFIX'] ?? '-glibc-2.28-gcc-8.5.0'; + const prefix = process.env['VSCODE_SYSROOT_PREFIX'] ?? '-glibc-2.28-gcc-10.5.0'; switch (arch) { case 'amd64': expectedName = `x86_64-linux-gnu${prefix}.tar.gz`; @@ -172,7 +172,7 @@ export async function getVSCodeSysroot(arch: DebianArchString, isMusl: boolean = } console.log(`Installing ${arch} root image: ${sysroot}`); fs.rmSync(sysroot, { recursive: true, force: true }); - fs.mkdirSync(sysroot); + fs.mkdirSync(sysroot, { recursive: true }); await fetchUrl({ checksumSha256, assetName: expectedName, diff --git a/build/linux/dependencies-generator.js b/build/linux/dependencies-generator.js index 448ab38c4a4..7521729a8f2 100644 --- a/build/linux/dependencies-generator.js +++ b/build/linux/dependencies-generator.js @@ -26,7 +26,7 @@ const product = require("../../product.json"); // The reference dependencies, which one has to update when the new dependencies // are valid, are in dep-lists.ts const FAIL_BUILD_FOR_NEW_DEPENDENCIES = true; -// Based on https://source.chromium.org/chromium/chromium/src/+/refs/tags/132.0.6834.210:chrome/installer/linux/BUILD.gn;l=64-80 +// Based on https://source.chromium.org/chromium/chromium/src/+/refs/tags/134.0.6998.205:chrome/installer/linux/BUILD.gn;l=64-80 // and the Linux Archive build // Shared library dependencies that we already bundle. const bundledDeps = [ diff --git a/build/linux/dependencies-generator.ts b/build/linux/dependencies-generator.ts index 6c1f7b7570b..9383703580f 100644 --- a/build/linux/dependencies-generator.ts +++ b/build/linux/dependencies-generator.ts @@ -25,7 +25,7 @@ import product = require('../../product.json'); // are valid, are in dep-lists.ts const FAIL_BUILD_FOR_NEW_DEPENDENCIES: boolean = true; -// Based on https://source.chromium.org/chromium/chromium/src/+/refs/tags/132.0.6834.210:chrome/installer/linux/BUILD.gn;l=64-80 +// Based on https://source.chromium.org/chromium/chromium/src/+/refs/tags/134.0.6998.205:chrome/installer/linux/BUILD.gn;l=64-80 // and the Linux Archive build // Shared library dependencies that we already bundle. const bundledDeps = [ diff --git a/build/linux/rpm/dep-lists.js b/build/linux/rpm/dep-lists.js index f45b6f34b84..1f19c85017d 100644 --- a/build/linux/rpm/dep-lists.js +++ b/build/linux/rpm/dep-lists.js @@ -208,6 +208,7 @@ exports.referenceGeneratedDepsByArch = { 'libstdc++.so.6(GLIBCXX_3.4.20)', 'libstdc++.so.6(GLIBCXX_3.4.21)', 'libstdc++.so.6(GLIBCXX_3.4.22)', + 'libstdc++.so.6(GLIBCXX_3.4.26)', 'libstdc++.so.6(GLIBCXX_3.4.5)', 'libstdc++.so.6(GLIBCXX_3.4.9)', 'libudev.so.1', @@ -299,6 +300,7 @@ exports.referenceGeneratedDepsByArch = { 'libstdc++.so.6(GLIBCXX_3.4.20)(64bit)', 'libstdc++.so.6(GLIBCXX_3.4.21)(64bit)', 'libstdc++.so.6(GLIBCXX_3.4.22)(64bit)', + 'libstdc++.so.6(GLIBCXX_3.4.26)(64bit)', 'libstdc++.so.6(GLIBCXX_3.4.5)(64bit)', 'libstdc++.so.6(GLIBCXX_3.4.9)(64bit)', 'libudev.so.1()(64bit)', diff --git a/build/linux/rpm/dep-lists.ts b/build/linux/rpm/dep-lists.ts index d277ca7e664..db523385941 100644 --- a/build/linux/rpm/dep-lists.ts +++ b/build/linux/rpm/dep-lists.ts @@ -207,6 +207,7 @@ export const referenceGeneratedDepsByArch = { 'libstdc++.so.6(GLIBCXX_3.4.20)', 'libstdc++.so.6(GLIBCXX_3.4.21)', 'libstdc++.so.6(GLIBCXX_3.4.22)', + 'libstdc++.so.6(GLIBCXX_3.4.26)', 'libstdc++.so.6(GLIBCXX_3.4.5)', 'libstdc++.so.6(GLIBCXX_3.4.9)', 'libudev.so.1', @@ -298,6 +299,7 @@ export const referenceGeneratedDepsByArch = { 'libstdc++.so.6(GLIBCXX_3.4.20)(64bit)', 'libstdc++.so.6(GLIBCXX_3.4.21)(64bit)', 'libstdc++.so.6(GLIBCXX_3.4.22)(64bit)', + 'libstdc++.so.6(GLIBCXX_3.4.26)(64bit)', 'libstdc++.so.6(GLIBCXX_3.4.5)(64bit)', 'libstdc++.so.6(GLIBCXX_3.4.9)(64bit)', 'libudev.so.1()(64bit)', diff --git a/build/npm/postinstall.js b/build/npm/postinstall.js index 458847afac5..be0c6c24282 100644 --- a/build/npm/postinstall.js +++ b/build/npm/postinstall.js @@ -162,26 +162,8 @@ for (let dir of dirs) { if (process.env['VSCODE_REMOTE_LDFLAGS']) { opts.env['LDFLAGS'] = process.env['VSCODE_REMOTE_LDFLAGS']; } if (process.env['VSCODE_REMOTE_NODE_GYP']) { opts.env['npm_config_node_gyp'] = process.env['VSCODE_REMOTE_NODE_GYP']; } - const globalGypPath = path.join(os.homedir(), '.gyp'); - const globalInclude = path.join(globalGypPath, 'include.gypi'); - const tempGlobalInclude = path.join(globalGypPath, 'include.gypi.bak'); - if (process.platform === 'linux' && - (process.env['CI'] || process.env['BUILD_ARTIFACTSTAGINGDIRECTORY'])) { - // Following include file rename should be removed - // when `Override gnu target for arm64 and arm` step - // is removed from the product build pipeline. - if (fs.existsSync(globalInclude)) { - fs.renameSync(globalInclude, tempGlobalInclude); - } - } setNpmrcConfig('remote', opts.env); npmInstall(dir, opts); - if (process.platform === 'linux' && - (process.env['CI'] || process.env['BUILD_ARTIFACTSTAGINGDIRECTORY'])) { - if (fs.existsSync(tempGlobalInclude)) { - fs.renameSync(tempGlobalInclude, globalInclude); - } - } continue; } diff --git a/build/package-lock.json b/build/package-lock.json index 4d18fff30af..12850d22160 100644 --- a/build/package-lock.json +++ b/build/package-lock.json @@ -34,7 +34,7 @@ "@types/minimatch": "^3.0.3", "@types/minimist": "^1.2.1", "@types/mocha": "^9.1.1", - "@types/node": "20.x", + "@types/node": "22.x", "@types/pump": "^1.0.1", "@types/rimraf": "^2.0.4", "@types/through": "^0.0.29", @@ -1193,13 +1193,13 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.17.27", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.27.tgz", - "integrity": "sha512-U58sbKhDrthHlxHRJw7ZLiLDZGmAUOZUbpw0S6nL27sYUdhvgBLCRu/keSd6qcTsfArd1sRFCCBxzWATGr/0UA==", + "version": "22.13.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", + "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.19.2" + "undici-types": "~6.20.0" } }, "node_modules/@types/pump": { @@ -4388,9 +4388,9 @@ "dev": true }, "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "dev": true, "license": "MIT" }, diff --git a/build/package.json b/build/package.json index cebae7e286d..c19196d45c7 100644 --- a/build/package.json +++ b/build/package.json @@ -28,7 +28,7 @@ "@types/minimatch": "^3.0.3", "@types/minimist": "^1.2.1", "@types/mocha": "^9.1.1", - "@types/node": "20.x", + "@types/node": "22.x", "@types/pump": "^1.0.1", "@types/rimraf": "^2.0.4", "@types/through": "^0.0.29", diff --git a/cgmanifest.json b/cgmanifest.json index 6ee72b3f757..cf6314c7bd1 100644 --- a/cgmanifest.json +++ b/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "chromium", "repositoryUrl": "https://chromium.googlesource.com/chromium/src", - "commitHash": "5c0cb964bca15fcf41718d54f4b8d70d6b9079de" + "commitHash": "87c50a22ef6b7c370b7351cfecbed498e3ae894d" } }, "licenseDetail": [ @@ -40,7 +40,7 @@ "SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." ], "isOnlyProductionDependency": true, - "version": "132.0.6834.210" + "version": "134.0.6998.205" }, { "component": { @@ -516,11 +516,11 @@ "git": { "name": "nodejs", "repositoryUrl": "https://github.com/nodejs/node", - "commitHash": "bb1a61d8737feff534bb85368dab3b7c554c863d" + "commitHash": "5d2feb257bcee090e57900eb51720171a6aa92f3" } }, "isOnlyProductionDependency": true, - "version": "20.19.0" + "version": "22.14.0" }, { "component": { @@ -528,12 +528,12 @@ "git": { "name": "electron", "repositoryUrl": "https://github.com/electron/electron", - "commitHash": "d0594707ded4d564c95badf5322d5893295da4ed" + "commitHash": "bdde6689264aad24e6ec402d443384fc949d891f" } }, "isOnlyProductionDependency": true, "license": "MIT", - "version": "34.5.1" + "version": "35.2.2" }, { "component": { diff --git a/eslint.config.js b/eslint.config.js index 9e35e52ec95..6131094d6a1 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1431,79 +1431,4 @@ export default tseslint.config( '@typescript-eslint/prefer-readonly': 'warn', } }, - // Prompt files related code - { - files: [ - 'src/vs/platform/prompts/**/*.ts', - 'src/vs/editor/common/codecs/**/*.ts', - 'src/vs/workbench/contrib/chat/common/promptSyntax/**/*.ts', - ], - languageOptions: { - parser: tseslint.parser, - parserOptions: { - project: 'src/vs/platform/prompts/tsconfig.strict.json', - } - }, - plugins: { - '@typescript-eslint': tseslint.plugin, - '@stylistic/ts': stylisticTs, - }, - rules: { - '@typescript-eslint/prefer-readonly': 'warn', - '@typescript-eslint/await-thenable': 'error', - '@typescript-eslint/consistent-type-assertions': ['error', { 'assertionStyle': 'never' }], - '@typescript-eslint/explicit-function-return-type': [ - 'error', - { - allowDirectConstAssertionInArrowFunctions: false, - }, - ], - '@typescript-eslint/explicit-member-accessibility': [ - 'error', - { - accessibility: 'explicit', - ignoredMethodNames: ['constructor'], - }, - ], - 'no-shadow': 'off', '@typescript-eslint/no-shadow': 'error', - '@typescript-eslint/ban-ts-comment': 'error', - 'default-param-last': 'off', '@typescript-eslint/default-param-last': 'error', - 'no-array-constructor': 'off', '@typescript-eslint/no-array-constructor': 'error', - '@typescript-eslint/explicit-module-boundary-types': 'error', - '@typescript-eslint/no-array-delete': 'error', - '@typescript-eslint/no-base-to-string': 'error', - '@typescript-eslint/no-confusing-non-null-assertion': 'error', - '@typescript-eslint/no-confusing-void-expression': 'error', - '@typescript-eslint/no-duplicate-enum-values': 'error', - '@typescript-eslint/no-dynamic-delete': 'error', - 'no-empty-function': 'off', '@typescript-eslint/no-empty-function': [ - 'error', { 'allow': ['private-constructors'] } - ], - '@typescript-eslint/no-empty-object-type': 'error', - '@typescript-eslint/no-explicit-any': ['error', { 'ignoreRestArgs': true }], - '@typescript-eslint/no-extra-non-null-assertion': 'error', - '@typescript-eslint/no-extraneous-class': 'error', - '@typescript-eslint/no-for-in-array': 'error', - 'no-implied-eval': 'off', '@typescript-eslint/no-implied-eval': 'error', - '@typescript-eslint/no-invalid-void-type': 'error', - 'no-loop-func': 'off', '@typescript-eslint/no-loop-func': 'error', - '@typescript-eslint/no-misused-new': 'warn', - '@typescript-eslint/no-mixed-enums': 'error', - '@typescript-eslint/no-floating-promises': 'error', - '@typescript-eslint/no-misused-promises': 'error', - '@typescript-eslint/no-non-null-asserted-nullish-coalescing': 'error', - '@typescript-eslint/no-non-null-asserted-optional-chain': 'error', - '@typescript-eslint/no-non-null-assertion': 'error', - '@typescript-eslint/no-redundant-type-constituents': 'error', - '@typescript-eslint/naming-convention': [ - 'warn', - { 'selector': 'variable', 'format': ['camelCase', 'UPPER_CASE', 'PascalCase'] }, - { 'selector': 'variable', 'filter': '^I.+Service$', 'format': ['PascalCase'], 'prefix': ['I'] }, - { 'selector': 'enumMember', 'format': ['PascalCase'] }, - { 'selector': 'typeAlias', 'format': ['PascalCase'], 'prefix': ['T'] }, - { 'selector': 'interface', 'format': ['PascalCase'], 'prefix': ['I'] } - ], - 'comma-dangle': ['warn', 'only-multiline'], - } - }, ); diff --git a/extensions/configuration-editing/package-lock.json b/extensions/configuration-editing/package-lock.json index cba3a0fece6..831c339ad94 100644 --- a/extensions/configuration-editing/package-lock.json +++ b/extensions/configuration-editing/package-lock.json @@ -14,7 +14,7 @@ "tunnel": "^0.0.6" }, "devDependencies": { - "@types/node": "20.x" + "@types/node": "22.x" }, "engines": { "vscode": "^1.0.0" @@ -175,12 +175,13 @@ } }, "node_modules/@types/node": { - "version": "20.11.24", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.24.tgz", - "integrity": "sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long==", + "version": "22.13.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", + "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.20.0" } }, "node_modules/before-after-hook": { @@ -219,10 +220,11 @@ } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" }, "node_modules/universal-user-agent": { "version": "7.0.2", diff --git a/extensions/configuration-editing/package.json b/extensions/configuration-editing/package.json index 531ba21fe06..18ca50055a0 100644 --- a/extensions/configuration-editing/package.json +++ b/extensions/configuration-editing/package.json @@ -169,7 +169,7 @@ ] }, "devDependencies": { - "@types/node": "20.x" + "@types/node": "22.x" }, "repository": { "type": "git", diff --git a/extensions/css-language-features/package-lock.json b/extensions/css-language-features/package-lock.json index 7ffe55ba0bc..87eb3b9e825 100644 --- a/extensions/css-language-features/package-lock.json +++ b/extensions/css-language-features/package-lock.json @@ -13,20 +13,20 @@ "vscode-uri": "^3.1.0" }, "devDependencies": { - "@types/node": "20.x" + "@types/node": "22.x" }, "engines": { "vscode": "^1.77.0" } }, "node_modules/@types/node": { - "version": "20.17.27", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.27.tgz", - "integrity": "sha512-U58sbKhDrthHlxHRJw7ZLiLDZGmAUOZUbpw0S6nL27sYUdhvgBLCRu/keSd6qcTsfArd1sRFCCBxzWATGr/0UA==", + "version": "22.13.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", + "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.19.2" + "undici-types": "~6.20.0" } }, "node_modules/balanced-match": { @@ -72,9 +72,9 @@ } }, "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "dev": true, "license": "MIT" }, diff --git a/extensions/css-language-features/package.json b/extensions/css-language-features/package.json index a05f7af687e..ca268873634 100644 --- a/extensions/css-language-features/package.json +++ b/extensions/css-language-features/package.json @@ -998,7 +998,7 @@ "vscode-uri": "^3.1.0" }, "devDependencies": { - "@types/node": "20.x" + "@types/node": "22.x" }, "repository": { "type": "git", diff --git a/extensions/css-language-features/server/package-lock.json b/extensions/css-language-features/server/package-lock.json index da345e38337..911549c278f 100644 --- a/extensions/css-language-features/server/package-lock.json +++ b/extensions/css-language-features/server/package-lock.json @@ -16,7 +16,7 @@ }, "devDependencies": { "@types/mocha": "^9.1.1", - "@types/node": "20.x" + "@types/node": "22.x" }, "engines": { "node": "*" @@ -29,12 +29,13 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.11.24", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.24.tgz", - "integrity": "sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long==", + "version": "22.13.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", + "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.20.0" } }, "node_modules/@vscode/l10n": { @@ -43,10 +44,11 @@ "integrity": "sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ==" }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" }, "node_modules/vscode-css-languageservice": { "version": "6.3.5", diff --git a/extensions/css-language-features/server/package.json b/extensions/css-language-features/server/package.json index c63252f6d8d..d943314e6ce 100644 --- a/extensions/css-language-features/server/package.json +++ b/extensions/css-language-features/server/package.json @@ -17,7 +17,7 @@ }, "devDependencies": { "@types/mocha": "^9.1.1", - "@types/node": "20.x" + "@types/node": "22.x" }, "scripts": { "compile": "gulp compile-extension:css-language-features-server", diff --git a/extensions/debug-auto-launch/package-lock.json b/extensions/debug-auto-launch/package-lock.json index d6a69a857f5..a25a1d9a1b4 100644 --- a/extensions/debug-auto-launch/package-lock.json +++ b/extensions/debug-auto-launch/package-lock.json @@ -9,26 +9,26 @@ "version": "1.0.0", "license": "MIT", "devDependencies": { - "@types/node": "20.x" + "@types/node": "22.x" }, "engines": { "vscode": "^1.5.0" } }, "node_modules/@types/node": { - "version": "20.17.27", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.27.tgz", - "integrity": "sha512-U58sbKhDrthHlxHRJw7ZLiLDZGmAUOZUbpw0S6nL27sYUdhvgBLCRu/keSd6qcTsfArd1sRFCCBxzWATGr/0UA==", + "version": "22.13.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", + "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.19.2" + "undici-types": "~6.20.0" } }, "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "dev": true, "license": "MIT" } diff --git a/extensions/debug-auto-launch/package.json b/extensions/debug-auto-launch/package.json index 4a5d3361f95..e1748672471 100644 --- a/extensions/debug-auto-launch/package.json +++ b/extensions/debug-auto-launch/package.json @@ -33,7 +33,7 @@ ] }, "devDependencies": { - "@types/node": "20.x" + "@types/node": "22.x" }, "prettier": { "printWidth": 100, diff --git a/extensions/debug-server-ready/package-lock.json b/extensions/debug-server-ready/package-lock.json index 29a149e0e16..fa4d0022466 100644 --- a/extensions/debug-server-ready/package-lock.json +++ b/extensions/debug-server-ready/package-lock.json @@ -9,26 +9,28 @@ "version": "1.0.0", "license": "MIT", "devDependencies": { - "@types/node": "20.x" + "@types/node": "22.x" }, "engines": { "vscode": "^1.32.0" } }, "node_modules/@types/node": { - "version": "20.11.24", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.24.tgz", - "integrity": "sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long==", + "version": "22.13.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", + "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.20.0" } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" } } } diff --git a/extensions/debug-server-ready/package.json b/extensions/debug-server-ready/package.json index 2afe977a9fc..adc91350ddc 100644 --- a/extensions/debug-server-ready/package.json +++ b/extensions/debug-server-ready/package.json @@ -212,7 +212,7 @@ ] }, "devDependencies": { - "@types/node": "20.x" + "@types/node": "22.x" }, "repository": { "type": "git", diff --git a/extensions/emmet/package-lock.json b/extensions/emmet/package-lock.json index 971528b395b..e225308e6d7 100644 --- a/extensions/emmet/package-lock.json +++ b/extensions/emmet/package-lock.json @@ -17,7 +17,7 @@ "vscode-languageserver-textdocument": "^1.0.1" }, "devDependencies": { - "@types/node": "20.x" + "@types/node": "22.x" }, "engines": { "vscode": "^1.13.0" @@ -81,13 +81,13 @@ "integrity": "sha1-JEywLHfsLnT3ipvTGCGKvJxQCmE= sha512-ZsZ2I9Vzso3Ho/pjZFsmmZ++FWeEd/txqybHTm4OgaZzdS8V9V/YYWQwg5TC38Z7uLWUV1vavpLLbjJtKubR1A==" }, "node_modules/@types/node": { - "version": "20.17.27", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.27.tgz", - "integrity": "sha512-U58sbKhDrthHlxHRJw7ZLiLDZGmAUOZUbpw0S6nL27sYUdhvgBLCRu/keSd6qcTsfArd1sRFCCBxzWATGr/0UA==", + "version": "22.13.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", + "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.19.2" + "undici-types": "~6.20.0" } }, "node_modules/@vscode/emmet-helper": { @@ -152,9 +152,9 @@ } }, "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "dev": true, "license": "MIT" }, diff --git a/extensions/emmet/package.json b/extensions/emmet/package.json index a390a86fc2e..5b5d3748f87 100644 --- a/extensions/emmet/package.json +++ b/extensions/emmet/package.json @@ -479,7 +479,7 @@ "deps": "npm install @vscode/emmet-helper" }, "devDependencies": { - "@types/node": "20.x" + "@types/node": "22.x" }, "dependencies": { "@emmetio/css-parser": "ramya-rao-a/css-parser#vscode", diff --git a/extensions/extension-editing/package-lock.json b/extensions/extension-editing/package-lock.json index 3fa0c35e2d0..4328e1ce81e 100644 --- a/extensions/extension-editing/package-lock.json +++ b/extensions/extension-editing/package-lock.json @@ -15,7 +15,7 @@ }, "devDependencies": { "@types/markdown-it": "0.0.2", - "@types/node": "20.x" + "@types/node": "22.x" }, "engines": { "vscode": "^1.4.0" @@ -28,12 +28,13 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.11.24", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.24.tgz", - "integrity": "sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long==", + "version": "22.13.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", + "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.20.0" } }, "node_modules/argparse": { @@ -101,10 +102,11 @@ "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" } } } diff --git a/extensions/extension-editing/package.json b/extensions/extension-editing/package.json index 184d28e8df0..c13a3386f5f 100644 --- a/extensions/extension-editing/package.json +++ b/extensions/extension-editing/package.json @@ -67,7 +67,7 @@ }, "devDependencies": { "@types/markdown-it": "0.0.2", - "@types/node": "20.x" + "@types/node": "22.x" }, "repository": { "type": "git", diff --git a/extensions/git-base/package-lock.json b/extensions/git-base/package-lock.json index f4b29739ca6..8ae6f0c2f7a 100644 --- a/extensions/git-base/package-lock.json +++ b/extensions/git-base/package-lock.json @@ -9,26 +9,28 @@ "version": "1.0.0", "license": "MIT", "devDependencies": { - "@types/node": "20.x" + "@types/node": "22.x" }, "engines": { "vscode": "0.10.x" } }, "node_modules/@types/node": { - "version": "20.11.24", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.24.tgz", - "integrity": "sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long==", + "version": "22.13.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", + "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.20.0" } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" } } } diff --git a/extensions/git-base/package.json b/extensions/git-base/package.json index 3c9b07a13e8..f61e5f8df56 100644 --- a/extensions/git-base/package.json +++ b/extensions/git-base/package.json @@ -104,7 +104,7 @@ ] }, "devDependencies": { - "@types/node": "20.x" + "@types/node": "22.x" }, "repository": { "type": "git", diff --git a/extensions/git/package-lock.json b/extensions/git/package-lock.json index 16b71e40c37..4f119e2c3f8 100644 --- a/extensions/git/package-lock.json +++ b/extensions/git/package-lock.json @@ -20,7 +20,7 @@ "devDependencies": { "@types/byline": "4.2.31", "@types/mocha": "^9.1.1", - "@types/node": "20.x", + "@types/node": "22.x", "@types/picomatch": "2.3.0", "@types/which": "3.0.0" }, @@ -182,13 +182,13 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.17.27", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.27.tgz", - "integrity": "sha512-U58sbKhDrthHlxHRJw7ZLiLDZGmAUOZUbpw0S6nL27sYUdhvgBLCRu/keSd6qcTsfArd1sRFCCBxzWATGr/0UA==", + "version": "22.13.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", + "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.19.2" + "undici-types": "~6.20.0" } }, "node_modules/@types/picomatch": { @@ -384,9 +384,9 @@ } }, "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "dev": true, "license": "MIT" }, diff --git a/extensions/git/package.json b/extensions/git/package.json index 21e90d59ee4..c85b212746c 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -3591,7 +3591,7 @@ "devDependencies": { "@types/byline": "4.2.31", "@types/mocha": "^9.1.1", - "@types/node": "20.x", + "@types/node": "22.x", "@types/picomatch": "2.3.0", "@types/which": "3.0.0" }, diff --git a/extensions/github-authentication/package-lock.json b/extensions/github-authentication/package-lock.json index cbc9e16b75f..60fbdfe141c 100644 --- a/extensions/github-authentication/package-lock.json +++ b/extensions/github-authentication/package-lock.json @@ -15,7 +15,7 @@ }, "devDependencies": { "@types/mocha": "^9.1.1", - "@types/node": "20.x", + "@types/node": "22.x", "@types/node-fetch": "^2.5.7" }, "engines": { @@ -153,12 +153,13 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.11.24", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.24.tgz", - "integrity": "sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long==", + "version": "22.13.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", + "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.20.0" } }, "node_modules/@types/node-fetch": { @@ -277,10 +278,11 @@ "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" }, "node_modules/vscode-tas-client": { "version": "0.1.84", diff --git a/extensions/github-authentication/package.json b/extensions/github-authentication/package.json index 96fbcd75d40..0dc940131b4 100644 --- a/extensions/github-authentication/package.json +++ b/extensions/github-authentication/package.json @@ -17,6 +17,9 @@ "ui", "workspace" ], + "enabledApiProposals": [ + "authIssuers" + ], "activationEvents": [], "capabilities": { "virtualWorkspaces": true, @@ -31,11 +34,17 @@ "authentication": [ { "label": "GitHub", - "id": "github" + "id": "github", + "issuerGlobs": [ + "https://github.com/login/oauth" + ] }, { "label": "GitHub Enterprise Server", - "id": "github-enterprise" + "id": "github-enterprise", + "issuerGlobs": [ + "*" + ] } ], "configuration": [{ @@ -67,7 +76,7 @@ }, "devDependencies": { "@types/mocha": "^9.1.1", - "@types/node": "20.x", + "@types/node": "22.x", "@types/node-fetch": "^2.5.7" }, "repository": { diff --git a/extensions/github-authentication/src/extension.ts b/extensions/github-authentication/src/extension.ts index 62f69f85b56..88185405ec0 100644 --- a/extensions/github-authentication/src/extension.ts +++ b/extensions/github-authentication/src/extension.ts @@ -6,10 +6,42 @@ import * as vscode from 'vscode'; import { GitHubAuthenticationProvider, UriEventHandler } from './github'; -function initGHES(context: vscode.ExtensionContext, uriHandler: UriEventHandler) { +const settingNotSent = '"github-enterprise.uri" not set'; +const settingInvalid = '"github-enterprise.uri" invalid'; + +class NullAuthProvider implements vscode.AuthenticationProvider { + private _onDidChangeSessions = new vscode.EventEmitter(); + onDidChangeSessions = this._onDidChangeSessions.event; + + private readonly _disposable: vscode.Disposable; + + constructor(private readonly _errorMessage: string) { + this._disposable = vscode.authentication.registerAuthenticationProvider('github-enterprise', 'GitHub Enterprise', this); + } + + createSession(): Thenable { + throw new Error(this._errorMessage); + } + + getSessions(): Thenable { + return Promise.resolve([]); + } + removeSession(): Thenable { + throw new Error(this._errorMessage); + } + + dispose() { + this._onDidChangeSessions.dispose(); + this._disposable.dispose(); + } +} + +function initGHES(context: vscode.ExtensionContext, uriHandler: UriEventHandler): vscode.Disposable { const settingValue = vscode.workspace.getConfiguration().get('github-enterprise.uri'); if (!settingValue) { - return undefined; + const provider = new NullAuthProvider(settingNotSent); + context.subscriptions.push(provider); + return provider; } // validate user value @@ -18,7 +50,9 @@ function initGHES(context: vscode.ExtensionContext, uriHandler: UriEventHandler) uri = vscode.Uri.parse(settingValue, true); } catch (e) { vscode.window.showErrorMessage(vscode.l10n.t('GitHub Enterprise Server URI is not a valid URI: {0}', e.message ?? e)); - return; + const provider = new NullAuthProvider(settingInvalid); + context.subscriptions.push(provider); + return provider; } const githubEnterpriseAuthProvider = new GitHubAuthenticationProvider(context, uriHandler, uri); @@ -33,12 +67,14 @@ export function activate(context: vscode.ExtensionContext) { context.subscriptions.push(new GitHubAuthenticationProvider(context, uriHandler)); - let githubEnterpriseAuthProvider: GitHubAuthenticationProvider | undefined = initGHES(context, uriHandler); - + let before = vscode.workspace.getConfiguration().get('github-enterprise.uri'); + let githubEnterpriseAuthProvider = initGHES(context, uriHandler); context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(async e => { if (e.affectsConfiguration('github-enterprise.uri')) { - if (vscode.workspace.getConfiguration().get('github-enterprise.uri')) { + const after = vscode.workspace.getConfiguration().get('github-enterprise.uri'); + if (before !== after) { githubEnterpriseAuthProvider?.dispose(); + before = after; githubEnterpriseAuthProvider = initGHES(context, uriHandler); } } diff --git a/extensions/github-authentication/src/github.ts b/extensions/github-authentication/src/github.ts index d67fc99e08b..c56cb96dae5 100644 --- a/extensions/github-authentication/src/github.ts +++ b/extensions/github-authentication/src/github.ts @@ -137,7 +137,17 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid this._disposable = vscode.Disposable.from( this._telemetryReporter, - vscode.authentication.registerAuthenticationProvider(type, this._githubServer.friendlyName, this, { supportsMultipleAccounts: true }), + vscode.authentication.registerAuthenticationProvider( + type, + this._githubServer.friendlyName, + this, + { + supportsMultipleAccounts: true, + supportedIssuers: [ + ghesUri ?? vscode.Uri.parse('https://github.com/login/oauth') + ] + } + ), this.context.secrets.onDidChange(() => this.checkForUpdates()) ); } diff --git a/extensions/github-authentication/src/githubServer.ts b/extensions/github-authentication/src/githubServer.ts index a9f94ccf65d..a2ddbdc548f 100644 --- a/extensions/github-authentication/src/githubServer.ts +++ b/extensions/github-authentication/src/githubServer.ts @@ -114,11 +114,12 @@ export class GitHubServer implements IGitHubServer { const supportedClient = isSupportedClient(callbackUri); const supportedTarget = isSupportedTarget(this._type, this._ghesUri); + const isNodeEnvironment = typeof process !== 'undefined' && typeof process?.versions?.node === 'string'; const flows = getFlows({ target: this._type === AuthProviderType.github ? GitHubTarget.DotCom : supportedTarget ? GitHubTarget.HostedEnterprise : GitHubTarget.Enterprise, - extensionHost: typeof navigator === 'undefined' + extensionHost: isNodeEnvironment ? this._extensionKind === vscode.ExtensionKind.UI ? ExtensionHost.Local : ExtensionHost.Remote : ExtensionHost.WebWorker, isSupportedClient: supportedClient diff --git a/extensions/github-authentication/tsconfig.json b/extensions/github-authentication/tsconfig.json index 5e4713e9f3b..841f6f09adf 100644 --- a/extensions/github-authentication/tsconfig.json +++ b/extensions/github-authentication/tsconfig.json @@ -12,6 +12,7 @@ }, "include": [ "src/**/*", - "../../src/vscode-dts/vscode.d.ts" + "../../src/vscode-dts/vscode.d.ts", + "../../src/vscode-dts/vscode.proposed.authIssuers.d.ts" ] } diff --git a/extensions/github/package-lock.json b/extensions/github/package-lock.json index 314527b3771..d6d6dcd964e 100644 --- a/extensions/github/package-lock.json +++ b/extensions/github/package-lock.json @@ -16,7 +16,7 @@ "tunnel": "^0.0.6" }, "devDependencies": { - "@types/node": "20.x" + "@types/node": "22.x" }, "engines": { "vscode": "^1.41.0" @@ -363,12 +363,13 @@ } }, "node_modules/@types/node": { - "version": "20.11.24", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.24.tgz", - "integrity": "sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long==", + "version": "22.13.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", + "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.20.0" } }, "node_modules/@vscode/extension-telemetry": { @@ -440,10 +441,11 @@ } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" }, "node_modules/universal-user-agent": { "version": "7.0.2", diff --git a/extensions/github/package.json b/extensions/github/package.json index 75cd0827bcb..726a882ebc3 100644 --- a/extensions/github/package.json +++ b/extensions/github/package.json @@ -235,7 +235,7 @@ "@vscode/extension-telemetry": "^1.0.0" }, "devDependencies": { - "@types/node": "20.x" + "@types/node": "22.x" }, "repository": { "type": "git", diff --git a/extensions/grunt/package-lock.json b/extensions/grunt/package-lock.json index f0431ef8c85..ca37ada8874 100644 --- a/extensions/grunt/package-lock.json +++ b/extensions/grunt/package-lock.json @@ -9,26 +9,28 @@ "version": "1.0.0", "license": "MIT", "devDependencies": { - "@types/node": "20.x" + "@types/node": "22.x" }, "engines": { "vscode": "*" } }, "node_modules/@types/node": { - "version": "20.11.24", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.24.tgz", - "integrity": "sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long==", + "version": "22.13.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", + "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.20.0" } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" } } } diff --git a/extensions/grunt/package.json b/extensions/grunt/package.json index ae533cc0e47..b0932676ec0 100644 --- a/extensions/grunt/package.json +++ b/extensions/grunt/package.json @@ -19,7 +19,7 @@ }, "dependencies": {}, "devDependencies": { - "@types/node": "20.x" + "@types/node": "22.x" }, "main": "./out/main", "activationEvents": [ diff --git a/extensions/gulp/package-lock.json b/extensions/gulp/package-lock.json index 9311609f962..2ded5fe7db1 100644 --- a/extensions/gulp/package-lock.json +++ b/extensions/gulp/package-lock.json @@ -9,26 +9,28 @@ "version": "1.0.0", "license": "MIT", "devDependencies": { - "@types/node": "20.x" + "@types/node": "22.x" }, "engines": { "vscode": "*" } }, "node_modules/@types/node": { - "version": "20.11.24", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.24.tgz", - "integrity": "sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long==", + "version": "22.13.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", + "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.20.0" } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" } } } diff --git a/extensions/gulp/package.json b/extensions/gulp/package.json index 0c19b688477..88c402468cf 100644 --- a/extensions/gulp/package.json +++ b/extensions/gulp/package.json @@ -18,7 +18,7 @@ }, "dependencies": {}, "devDependencies": { - "@types/node": "20.x" + "@types/node": "22.x" }, "main": "./out/main", "activationEvents": [ diff --git a/extensions/html-language-features/client/src/htmlClient.ts b/extensions/html-language-features/client/src/htmlClient.ts index 6e66f8da288..2b0f961899b 100644 --- a/extensions/html-language-features/client/src/htmlClient.ts +++ b/extensions/html-language-features/client/src/htmlClient.ts @@ -7,7 +7,8 @@ import { languages, ExtensionContext, Position, TextDocument, Range, CompletionItem, CompletionItemKind, SnippetString, workspace, extensions, Disposable, FormattingOptions, CancellationToken, ProviderResult, TextEdit, CompletionContext, CompletionList, SemanticTokensLegend, - DocumentSemanticTokensProvider, DocumentRangeSemanticTokensProvider, SemanticTokens, window, commands, OutputChannel, l10n + DocumentSemanticTokensProvider, DocumentRangeSemanticTokensProvider, SemanticTokens, window, commands, l10n, + LogOutputChannel } from 'vscode'; import { LanguageClientOptions, RequestType, DocumentRangeFormattingParams, @@ -90,12 +91,12 @@ export interface AsyncDisposable { export async function startClient(context: ExtensionContext, newLanguageClient: LanguageClientConstructor, runtime: Runtime): Promise { - const outputChannel = window.createOutputChannel(languageServerDescription); + const logOutputChannel = window.createOutputChannel(languageServerDescription, { log: true }); const languageParticipants = getLanguageParticipants(); context.subscriptions.push(languageParticipants); - let client: Disposable | undefined = await startClientWithParticipants(languageParticipants, newLanguageClient, outputChannel, runtime); + let client: Disposable | undefined = await startClientWithParticipants(languageParticipants, newLanguageClient, logOutputChannel, runtime); const promptForLinkedEditingKey = 'html.promptForLinkedEditing'; if (extensions.getExtension('formulahendry.auto-rename-tag') !== undefined && (context.globalState.get(promptForLinkedEditingKey) !== false)) { @@ -123,12 +124,12 @@ export async function startClient(context: ExtensionContext, newLanguageClient: } restartTrigger = runtime.timer.setTimeout(async () => { if (client) { - outputChannel.appendLine('Extensions have changed, restarting HTML server...'); - outputChannel.appendLine(''); + logOutputChannel.info('Extensions have changed, restarting HTML server...'); + logOutputChannel.info(''); const oldClient = client; client = undefined; await oldClient.dispose(); - client = await startClientWithParticipants(languageParticipants, newLanguageClient, outputChannel, runtime); + client = await startClientWithParticipants(languageParticipants, newLanguageClient, logOutputChannel, runtime); } }, 2000); }); @@ -137,12 +138,12 @@ export async function startClient(context: ExtensionContext, newLanguageClient: dispose: async () => { restartTrigger?.dispose(); await client?.dispose(); - outputChannel.dispose(); + logOutputChannel.dispose(); } }; } -async function startClientWithParticipants(languageParticipants: LanguageParticipants, newLanguageClient: LanguageClientConstructor, outputChannel: OutputChannel, runtime: Runtime): Promise { +async function startClientWithParticipants(languageParticipants: LanguageParticipants, newLanguageClient: LanguageClientConstructor, logOutputChannel: LogOutputChannel, runtime: Runtime): Promise { const toDispose: Disposable[] = []; @@ -188,7 +189,7 @@ async function startClientWithParticipants(languageParticipants: LanguagePartici } } }; - clientOptions.outputChannel = outputChannel; + clientOptions.outputChannel = logOutputChannel; // Create the language client and start the client. const client = newLanguageClient('html', languageServerDescription, clientOptions); diff --git a/extensions/html-language-features/package-lock.json b/extensions/html-language-features/package-lock.json index 39202ca37ec..882d41f2fc2 100644 --- a/extensions/html-language-features/package-lock.json +++ b/extensions/html-language-features/package-lock.json @@ -14,7 +14,7 @@ "vscode-uri": "^3.1.0" }, "devDependencies": { - "@types/node": "20.x" + "@types/node": "22.x" }, "engines": { "vscode": "^1.77.0" @@ -145,13 +145,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.17.27", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.27.tgz", - "integrity": "sha512-U58sbKhDrthHlxHRJw7ZLiLDZGmAUOZUbpw0S6nL27sYUdhvgBLCRu/keSd6qcTsfArd1sRFCCBxzWATGr/0UA==", + "version": "22.13.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", + "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.19.2" + "undici-types": "~6.20.0" } }, "node_modules/@vscode/extension-telemetry": { @@ -211,9 +211,9 @@ } }, "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "dev": true, "license": "MIT" }, diff --git a/extensions/html-language-features/package.json b/extensions/html-language-features/package.json index 1202f1d41c0..458264f7973 100644 --- a/extensions/html-language-features/package.json +++ b/extensions/html-language-features/package.json @@ -263,7 +263,7 @@ "vscode-uri": "^3.1.0" }, "devDependencies": { - "@types/node": "20.x" + "@types/node": "22.x" }, "repository": { "type": "git", diff --git a/extensions/html-language-features/server/package-lock.json b/extensions/html-language-features/server/package-lock.json index 0d0999f0382..d5260128791 100644 --- a/extensions/html-language-features/server/package-lock.json +++ b/extensions/html-language-features/server/package-lock.json @@ -18,7 +18,7 @@ }, "devDependencies": { "@types/mocha": "^9.1.1", - "@types/node": "20.x" + "@types/node": "22.x" }, "engines": { "node": "*" @@ -31,12 +31,13 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.12.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.5.tgz", - "integrity": "sha512-BD+BjQ9LS/D8ST9p5uqBxghlN+S42iuNxjsUGjeZobe/ciXzk2qb1B6IXc6AnRLS+yFJRpN2IPEHMzwspfDJNw==", + "version": "22.13.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", + "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.20.0" } }, "node_modules/@vscode/l10n": { @@ -45,10 +46,11 @@ "integrity": "sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ==" }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" }, "node_modules/vscode-css-languageservice": { "version": "6.3.5", diff --git a/extensions/html-language-features/server/package.json b/extensions/html-language-features/server/package.json index 883a17760b2..7759fa90245 100644 --- a/extensions/html-language-features/server/package.json +++ b/extensions/html-language-features/server/package.json @@ -18,7 +18,7 @@ }, "devDependencies": { "@types/mocha": "^9.1.1", - "@types/node": "20.x" + "@types/node": "22.x" }, "scripts": { "compile": "npx gulp compile-extension:html-language-features-server", diff --git a/extensions/ipynb/src/test/clearOutputs.test.ts b/extensions/ipynb/src/test/clearOutputs.test.ts index 62e40d6110d..fc2608ee227 100644 --- a/extensions/ipynb/src/test/clearOutputs.test.ts +++ b/extensions/ipynb/src/test/clearOutputs.test.ts @@ -25,7 +25,7 @@ suite(`ipynb Clear Outputs`, () => { await vscode.commands.executeCommand('workbench.action.closeAllEditors'); }); - test('Clear outputs after opening Notebook', async () => { + test.skip('Clear outputs after opening Notebook', async () => { const cells: nbformat.ICell[] = [ { cell_type: 'code', diff --git a/extensions/jake/package-lock.json b/extensions/jake/package-lock.json index ff50538c25e..c7d37035062 100644 --- a/extensions/jake/package-lock.json +++ b/extensions/jake/package-lock.json @@ -9,26 +9,28 @@ "version": "1.0.0", "license": "MIT", "devDependencies": { - "@types/node": "20.x" + "@types/node": "22.x" }, "engines": { "vscode": "*" } }, "node_modules/@types/node": { - "version": "20.11.24", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.24.tgz", - "integrity": "sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long==", + "version": "22.13.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", + "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.20.0" } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" } } } diff --git a/extensions/jake/package.json b/extensions/jake/package.json index 1d5d1250db0..e68c335d495 100644 --- a/extensions/jake/package.json +++ b/extensions/jake/package.json @@ -18,7 +18,7 @@ }, "dependencies": {}, "devDependencies": { - "@types/node": "20.x" + "@types/node": "22.x" }, "main": "./out/main", "activationEvents": [ diff --git a/extensions/json-language-features/package-lock.json b/extensions/json-language-features/package-lock.json index 85f31440359..71cdc676594 100644 --- a/extensions/json-language-features/package-lock.json +++ b/extensions/json-language-features/package-lock.json @@ -14,7 +14,7 @@ "vscode-languageclient": "^10.0.0-next.14" }, "devDependencies": { - "@types/node": "20.x" + "@types/node": "22.x" }, "engines": { "vscode": "^1.77.0" @@ -145,12 +145,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.11.24", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.24.tgz", - "integrity": "sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long==", + "version": "22.13.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", + "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.20.0" } }, "node_modules/@vscode/extension-telemetry": { @@ -215,10 +216,11 @@ } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" }, "node_modules/vscode-jsonrpc": { "version": "9.0.0-next.7", diff --git a/extensions/json-language-features/package.json b/extensions/json-language-features/package.json index 375afed0ce6..728f29f25dd 100644 --- a/extensions/json-language-features/package.json +++ b/extensions/json-language-features/package.json @@ -173,7 +173,7 @@ "vscode-languageclient": "^10.0.0-next.14" }, "devDependencies": { - "@types/node": "20.x" + "@types/node": "22.x" }, "repository": { "type": "git", diff --git a/extensions/json-language-features/server/package-lock.json b/extensions/json-language-features/server/package-lock.json index a0b95ddd48f..5a8f0fe63d1 100644 --- a/extensions/json-language-features/server/package-lock.json +++ b/extensions/json-language-features/server/package-lock.json @@ -21,7 +21,7 @@ }, "devDependencies": { "@types/mocha": "^9.1.1", - "@types/node": "20.x" + "@types/node": "22.x" }, "engines": { "node": "*" @@ -34,12 +34,13 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.12.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.5.tgz", - "integrity": "sha512-BD+BjQ9LS/D8ST9p5uqBxghlN+S42iuNxjsUGjeZobe/ciXzk2qb1B6IXc6AnRLS+yFJRpN2IPEHMzwspfDJNw==", + "version": "22.13.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", + "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.20.0" } }, "node_modules/@vscode/l10n": { @@ -58,10 +59,11 @@ "integrity": "sha512-bH6E4PMmsEXYrLX6Kr1vu+xI3HproB1vECAwaPSJeroLE1kpWE3HR27uB4icx+6YORu1ajqBJXxuedv8ZQg5Lw==" }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" }, "node_modules/vscode-json-languageservice": { "version": "5.5.0", diff --git a/extensions/json-language-features/server/package.json b/extensions/json-language-features/server/package.json index 55deaff19e5..a72732d15ab 100644 --- a/extensions/json-language-features/server/package.json +++ b/extensions/json-language-features/server/package.json @@ -21,7 +21,7 @@ }, "devDependencies": { "@types/mocha": "^9.1.1", - "@types/node": "20.x" + "@types/node": "22.x" }, "scripts": { "prepublishOnly": "npm run clean && npm run compile", diff --git a/extensions/merge-conflict/package-lock.json b/extensions/merge-conflict/package-lock.json index 94caad8f57e..ee1581e78de 100644 --- a/extensions/merge-conflict/package-lock.json +++ b/extensions/merge-conflict/package-lock.json @@ -12,7 +12,7 @@ "@vscode/extension-telemetry": "^0.9.8" }, "devDependencies": { - "@types/node": "20.x" + "@types/node": "22.x" }, "engines": { "vscode": "^1.5.0" @@ -143,13 +143,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.17.27", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.27.tgz", - "integrity": "sha512-U58sbKhDrthHlxHRJw7ZLiLDZGmAUOZUbpw0S6nL27sYUdhvgBLCRu/keSd6qcTsfArd1sRFCCBxzWATGr/0UA==", + "version": "22.13.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", + "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.19.2" + "undici-types": "~6.20.0" } }, "node_modules/@vscode/extension-telemetry": { @@ -167,9 +167,9 @@ } }, "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "dev": true, "license": "MIT" } diff --git a/extensions/merge-conflict/package.json b/extensions/merge-conflict/package.json index de56c9c22cf..c699832992c 100644 --- a/extensions/merge-conflict/package.json +++ b/extensions/merge-conflict/package.json @@ -169,7 +169,7 @@ "@vscode/extension-telemetry": "^0.9.8" }, "devDependencies": { - "@types/node": "20.x" + "@types/node": "22.x" }, "repository": { "type": "git", diff --git a/extensions/microsoft-authentication/package-lock.json b/extensions/microsoft-authentication/package-lock.json index c500a8c3d3e..43600731960 100644 --- a/extensions/microsoft-authentication/package-lock.json +++ b/extensions/microsoft-authentication/package-lock.json @@ -17,7 +17,7 @@ "vscode-tas-client": "^0.1.84" }, "devDependencies": { - "@types/node": "20.x", + "@types/node": "22.x", "@types/node-fetch": "^2.5.7", "@types/randombytes": "^2.0.0", "@types/sha.js": "^2.4.0", @@ -202,13 +202,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.17.27", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.27.tgz", - "integrity": "sha512-U58sbKhDrthHlxHRJw7ZLiLDZGmAUOZUbpw0S6nL27sYUdhvgBLCRu/keSd6qcTsfArd1sRFCCBxzWATGr/0UA==", + "version": "22.13.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", + "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.19.2" + "undici-types": "~6.20.0" } }, "node_modules/@types/node-fetch": { @@ -457,9 +457,9 @@ "integrity": "sha512-V+uqV66BOQnWxvI6HjDnE4VkInmYZUQ4dgB7gzaDyFyFSK1i1nF/j7DpS9UbQAgV9NaF1XpcyuavnM1qOeiEIg==" }, "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "dev": true, "license": "MIT" }, diff --git a/extensions/microsoft-authentication/package.json b/extensions/microsoft-authentication/package.json index 1356583844e..90e530f5561 100644 --- a/extensions/microsoft-authentication/package.json +++ b/extensions/microsoft-authentication/package.json @@ -15,7 +15,8 @@ "activationEvents": [], "enabledApiProposals": [ "idToken", - "nativeWindowHandle" + "nativeWindowHandle", + "authIssuers" ], "capabilities": { "virtualWorkspaces": true, @@ -31,7 +32,10 @@ "authentication": [ { "label": "Microsoft", - "id": "microsoft" + "id": "microsoft", + "issuerGlobs": [ + "https://login.microsoftonline.com/*/v2.0" + ] }, { "label": "Microsoft Sovereign Cloud", @@ -131,7 +135,7 @@ "watch-web": "npx webpack-cli --config extension-browser.webpack.config --mode none --watch --info-verbosity verbose" }, "devDependencies": { - "@types/node": "20.x", + "@types/node": "22.x", "@types/node-fetch": "^2.5.7", "@types/randombytes": "^2.0.0", "@types/sha.js": "^2.4.0", diff --git a/extensions/microsoft-authentication/src/AADHelper.ts b/extensions/microsoft-authentication/src/AADHelper.ts index ad8dabe7533..d5316b1266c 100644 --- a/extensions/microsoft-authentication/src/AADHelper.ts +++ b/extensions/microsoft-authentication/src/AADHelper.ts @@ -182,7 +182,7 @@ export class AzureActiveDirectoryService { for (const token of this._tokens) { /* __GDPR__ - "login" : { + "account" : { "owner": "TylerLeonhardt", "comment": "Used to determine the usage of the Microsoft Auth Provider.", "scopes": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight", "comment": "Used to determine what scope combinations are being requested." }, @@ -203,7 +203,7 @@ export class AzureActiveDirectoryService { return this._sessionChangeEmitter.event; } - public getSessions(scopes?: string[], account?: vscode.AuthenticationSessionAccountInformation): Promise { + public getSessions(scopes: string[] | undefined, { account, issuer }: vscode.AuthenticationProviderSessionOptions = {}): Promise { if (!scopes) { this._logger.info('Getting sessions for all scopes...'); const sessions = this._tokens @@ -226,6 +226,12 @@ export class AzureActiveDirectoryService { if (!modifiedScopes.includes('offline_access')) { modifiedScopes.push('offline_access'); } + if (issuer) { + const tenant = issuer.path.split('/')[1]; + if (tenant) { + modifiedScopes.push(`VSCODE_TENANT:${tenant}`); + } + } modifiedScopes = modifiedScopes.sort(); const modifiedScopesStr = modifiedScopes.join(' '); @@ -237,7 +243,7 @@ export class AzureActiveDirectoryService { scopeStr: modifiedScopesStr, // filter our special scopes scopesToSend: modifiedScopes.filter(s => !s.startsWith('VSCODE_')).join(' '), - tenant: this.getTenantId(scopes), + tenant: this.getTenantId(modifiedScopes), }; this._logger.trace(`[${scopeData.scopeStr}] Queued getting sessions` + account ? ` for ${account?.label}` : ''); @@ -297,7 +303,7 @@ export class AzureActiveDirectoryService { .map(result => (result as PromiseFulfilledResult).value); } - public createSession(scopes: string[], account?: vscode.AuthenticationSessionAccountInformation): Promise { + public createSession(scopes: string[], { account, issuer }: vscode.AuthenticationProviderSessionOptions = {}): Promise { let modifiedScopes = [...scopes]; if (!modifiedScopes.includes('openid')) { modifiedScopes.push('openid'); @@ -311,6 +317,12 @@ export class AzureActiveDirectoryService { if (!modifiedScopes.includes('offline_access')) { modifiedScopes.push('offline_access'); } + if (issuer) { + const tenant = issuer.path.split('/')[1]; + if (tenant) { + modifiedScopes.push(`VSCODE_TENANT:${tenant}`); + } + } modifiedScopes = modifiedScopes.sort(); const scopeData: IScopeData = { originalScopes: scopes, @@ -319,7 +331,7 @@ export class AzureActiveDirectoryService { // filter our special scopes scopesToSend: modifiedScopes.filter(s => !s.startsWith('VSCODE_')).join(' '), clientId: this.getClientId(scopes), - tenant: this.getTenantId(scopes), + tenant: this.getTenantId(modifiedScopes), }; this._logger.trace(`[${scopeData.scopeStr}] Queued creating session`); diff --git a/extensions/microsoft-authentication/src/common/scopeData.ts b/extensions/microsoft-authentication/src/common/scopeData.ts index 88a0aad68cc..f3b4d37dbb6 100644 --- a/extensions/microsoft-authentication/src/common/scopeData.ts +++ b/extensions/microsoft-authentication/src/common/scopeData.ts @@ -3,6 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Uri } from 'vscode'; + const DEFAULT_CLIENT_ID = 'aebc6443-996d-45c2-90f0-388ff96faa56'; const DEFAULT_TENANT = 'organizations'; @@ -43,14 +45,14 @@ export class ScopeData { */ readonly tenantId: string | undefined; - constructor(readonly originalScopes: readonly string[] = []) { + constructor(readonly originalScopes: readonly string[] = [], issuer?: Uri) { const modifiedScopes = [...originalScopes]; modifiedScopes.sort(); this.allScopes = modifiedScopes; this.scopeStr = modifiedScopes.join(' '); this.scopesToSend = this.getScopesToSend(modifiedScopes); this.clientId = this.getClientId(this.allScopes); - this.tenant = this.getTenant(this.allScopes); + this.tenant = this.getTenant(this.allScopes, issuer); this.tenantId = this.getTenantId(this.tenant); } @@ -63,7 +65,14 @@ export class ScopeData { }, undefined) ?? DEFAULT_CLIENT_ID; } - private getTenant(scopes: string[]): string { + private getTenant(scopes: string[], issuer?: Uri): string { + if (issuer?.path) { + // Get tenant portion of URL + const tenant = issuer.path.split('/')[1]; + if (tenant) { + return tenant; + } + } return scopes.reduce((prev, current) => { if (current.startsWith('VSCODE_TENANT:')) { return current.split('VSCODE_TENANT:')[1]; diff --git a/extensions/microsoft-authentication/src/common/telemetryReporter.ts b/extensions/microsoft-authentication/src/common/telemetryReporter.ts index e18c743de9b..c9fde4a972c 100644 --- a/extensions/microsoft-authentication/src/common/telemetryReporter.ts +++ b/extensions/microsoft-authentication/src/common/telemetryReporter.ts @@ -99,7 +99,7 @@ export class MicrosoftAuthenticationTelemetryReporter implements IExperimentatio */ sendAccountEvent(scopes: string[], accountType: MicrosoftAccountType): void { /* __GDPR__ - "login" : { + "account" : { "owner": "TylerLeonhardt", "comment": "Used to determine the usage of the Microsoft Auth Provider.", "scopes": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight", "comment": "Used to determine what scope combinations are being requested." }, diff --git a/extensions/microsoft-authentication/src/common/test/scopeData.test.ts b/extensions/microsoft-authentication/src/common/test/scopeData.test.ts index 4c70e4fd07c..54eae0cb39b 100644 --- a/extensions/microsoft-authentication/src/common/test/scopeData.test.ts +++ b/extensions/microsoft-authentication/src/common/test/scopeData.test.ts @@ -5,6 +5,7 @@ import * as assert from 'assert'; import { ScopeData } from '../scopeData'; +import { Uri } from 'vscode'; suite('ScopeData', () => { test('should include default scopes if not present', () => { @@ -73,4 +74,22 @@ suite('ScopeData', () => { const scopeData = new ScopeData(['custom_scope', 'VSCODE_TENANT:some_guid']); assert.strictEqual(scopeData.tenantId, 'some_guid'); }); + + test('should extract tenant from issuer URL path', () => { + const issuer = Uri.parse('https://login.microsoftonline.com/tenant123/oauth2/v2.0'); + const scopeData = new ScopeData(['custom_scope'], issuer); + assert.strictEqual(scopeData.tenant, 'tenant123'); + }); + + test('should fallback to default tenant if issuer URL has no path segments', () => { + const issuer = Uri.parse('https://login.microsoftonline.com'); + const scopeData = new ScopeData(['custom_scope'], issuer); + assert.strictEqual(scopeData.tenant, 'organizations'); + }); + + test('should prioritize issuer URL over VSCODE_TENANT scope', () => { + const issuer = Uri.parse('https://login.microsoftonline.com/url_tenant/oauth2/v2.0'); + const scopeData = new ScopeData(['custom_scope', 'VSCODE_TENANT:scope_tenant'], issuer); + assert.strictEqual(scopeData.tenant, 'url_tenant'); + }); }); diff --git a/extensions/microsoft-authentication/src/extension.ts b/extensions/microsoft-authentication/src/extension.ts index 6a5fac9765d..487f76c2cdc 100644 --- a/extensions/microsoft-authentication/src/extension.ts +++ b/extensions/microsoft-authentication/src/extension.ts @@ -71,8 +71,9 @@ export async function activate(context: ExtensionContext) { commands.executeCommand('workbench.action.reloadWindow'); } })); + const isNodeEnvironment = typeof process !== 'undefined' && typeof process?.versions?.node === 'string'; // Only activate the new extension if we are not running in a browser environment - if (useMsal && typeof navigator === 'undefined') { + if (useMsal && isNodeEnvironment) { await extensionV2.activate(context, mainTelemetryReporter); } else { mainTelemetryReporter.sendActivatedWithClassicImplementationEvent(); diff --git a/extensions/microsoft-authentication/src/extensionV1.ts b/extensions/microsoft-authentication/src/extensionV1.ts index 370ea0c76b1..ba78ed2367c 100644 --- a/extensions/microsoft-authentication/src/extensionV1.ts +++ b/extensions/microsoft-authentication/src/extensionV1.ts @@ -62,7 +62,7 @@ async function initMicrosoftSovereignCloudAuthProvider(context: vscode.Extension createSession: async (scopes: string[]) => { try { /* __GDPR__ - "login" : { + "loginMicrosoftSovereignCloud" : { "owner": "TylerLeonhardt", "comment": "Used to determine the usage of the Microsoft Sovereign Cloud Auth Provider.", "scopes": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight", "comment": "Used to determine what scope combinations are being requested." } @@ -76,7 +76,7 @@ async function initMicrosoftSovereignCloudAuthProvider(context: vscode.Extension return await aadService.createSession(scopes); } catch (e) { /* __GDPR__ - "loginFailed" : { "owner": "TylerLeonhardt", "comment": "Used to determine how often users run into issues with the login flow." } + "loginMicrosoftSovereignCloudFailed" : { "owner": "TylerLeonhardt", "comment": "Used to determine how often users run into issues with the login flow." } */ telemetryReporter.sendTelemetryEvent('loginMicrosoftSovereignCloudFailed'); @@ -86,14 +86,14 @@ async function initMicrosoftSovereignCloudAuthProvider(context: vscode.Extension removeSession: async (id: string) => { try { /* __GDPR__ - "logout" : { "owner": "TylerLeonhardt", "comment": "Used to determine how often users log out." } + "logoutMicrosoftSovereignCloud" : { "owner": "TylerLeonhardt", "comment": "Used to determine how often users log out." } */ telemetryReporter.sendTelemetryEvent('logoutMicrosoftSovereignCloud'); await aadService.removeSessionById(id); } catch (e) { /* __GDPR__ - "logoutFailed" : { "owner": "TylerLeonhardt", "comment": "Used to determine how often fail to log out." } + "logoutMicrosoftSovereignCloudFailed" : { "owner": "TylerLeonhardt", "comment": "Used to determine how often fail to log out." } */ telemetryReporter.sendTelemetryEvent('logoutMicrosoftSovereignCloudFailed'); } @@ -122,49 +122,59 @@ export async function activate(context: vscode.ExtensionContext, telemetryReport Environment.AzureCloud); await loginService.initialize(); - context.subscriptions.push(vscode.authentication.registerAuthenticationProvider('microsoft', 'Microsoft', { - onDidChangeSessions: loginService.onDidChangeSessions, - getSessions: (scopes: string[], options?: vscode.AuthenticationProviderSessionOptions) => loginService.getSessions(scopes, options?.account), - createSession: async (scopes: string[], options?: vscode.AuthenticationProviderSessionOptions) => { - try { - /* __GDPR__ - "login" : { - "owner": "TylerLeonhardt", - "comment": "Used to determine the usage of the Microsoft Auth Provider.", - "scopes": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight", "comment": "Used to determine what scope combinations are being requested." } - } - */ - telemetryReporter.sendTelemetryEvent('login', { - // Get rid of guids from telemetry. - scopes: JSON.stringify(scopes.map(s => s.replace(/[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}/i, '{guid}'))), - }); + context.subscriptions.push(vscode.authentication.registerAuthenticationProvider( + 'microsoft', + 'Microsoft', + { + onDidChangeSessions: loginService.onDidChangeSessions, + getSessions: (scopes: string[], options?: vscode.AuthenticationProviderSessionOptions) => loginService.getSessions(scopes, options), + createSession: async (scopes: string[], options?: vscode.AuthenticationProviderSessionOptions) => { + try { + /* __GDPR__ + "login" : { + "owner": "TylerLeonhardt", + "comment": "Used to determine the usage of the Microsoft Auth Provider.", + "scopes": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight", "comment": "Used to determine what scope combinations are being requested." } + } + */ + telemetryReporter.sendTelemetryEvent('login', { + // Get rid of guids from telemetry. + scopes: JSON.stringify(scopes.map(s => s.replace(/[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}/i, '{guid}'))), + }); - return await loginService.createSession(scopes, options?.account); - } catch (e) { - /* __GDPR__ - "loginFailed" : { "owner": "TylerLeonhardt", "comment": "Used to determine how often users run into issues with the login flow." } - */ - telemetryReporter.sendTelemetryEvent('loginFailed'); + return await loginService.createSession(scopes, options); + } catch (e) { + /* __GDPR__ + "loginFailed" : { "owner": "TylerLeonhardt", "comment": "Used to determine how often users run into issues with the login flow." } + */ + telemetryReporter.sendTelemetryEvent('loginFailed'); - throw e; + throw e; + } + }, + removeSession: async (id: string) => { + try { + /* __GDPR__ + "logout" : { "owner": "TylerLeonhardt", "comment": "Used to determine how often users log out." } + */ + telemetryReporter.sendTelemetryEvent('logout'); + + await loginService.removeSessionById(id); + } catch (e) { + /* __GDPR__ + "logoutFailed" : { "owner": "TylerLeonhardt", "comment": "Used to determine how often fail to log out." } + */ + telemetryReporter.sendTelemetryEvent('logoutFailed'); + } } }, - removeSession: async (id: string) => { - try { - /* __GDPR__ - "logout" : { "owner": "TylerLeonhardt", "comment": "Used to determine how often users log out." } - */ - telemetryReporter.sendTelemetryEvent('logout'); - - await loginService.removeSessionById(id); - } catch (e) { - /* __GDPR__ - "logoutFailed" : { "owner": "TylerLeonhardt", "comment": "Used to determine how often fail to log out." } - */ - telemetryReporter.sendTelemetryEvent('logoutFailed'); - } + { + supportsMultipleAccounts: true, + supportedIssuers: [ + vscode.Uri.parse('https://login.microsoftonline.com/*/v2.0') + ] } - }, { supportsMultipleAccounts: true })); + )); let microsoftSovereignCloudAuthProviderDisposable = await initMicrosoftSovereignCloudAuthProvider(context, telemetryReporter, uriHandler, betterSecretStorage); diff --git a/extensions/microsoft-authentication/src/extensionV2.ts b/extensions/microsoft-authentication/src/extensionV2.ts index 978603ad132..2f6bc9551bf 100644 --- a/extensions/microsoft-authentication/src/extensionV2.ts +++ b/extensions/microsoft-authentication/src/extensionV2.ts @@ -7,7 +7,7 @@ import { Environment, EnvironmentParameters } from '@azure/ms-rest-azure-env'; import Logger from './logger'; import { MsalAuthProvider } from './node/authProvider'; import { UriEventHandler } from './UriEventHandler'; -import { authentication, commands, ExtensionContext, l10n, window, workspace, Disposable } from 'vscode'; +import { authentication, commands, ExtensionContext, l10n, window, workspace, Disposable, Uri } from 'vscode'; import { MicrosoftAuthenticationTelemetryReporter, MicrosoftSovereignCloudAuthenticationTelemetryReporter } from './common/telemetryReporter'; async function initMicrosoftSovereignCloudAuthProvider( @@ -79,7 +79,12 @@ export async function activate(context: ExtensionContext, mainTelemetryReporter: 'microsoft', 'Microsoft', authProvider, - { supportsMultipleAccounts: true } + { + supportsMultipleAccounts: true, + supportedIssuers: [ + Uri.parse('https://login.microsoftonline.com/*/v2.0') + ] + } )); let microsoftSovereignCloudAuthProviderDisposable = await initMicrosoftSovereignCloudAuthProvider(context, uriHandler); diff --git a/extensions/microsoft-authentication/src/node/authProvider.ts b/extensions/microsoft-authentication/src/node/authProvider.ts index 5ce9acd3e6a..2780374e2c4 100644 --- a/extensions/microsoft-authentication/src/node/authProvider.ts +++ b/extensions/microsoft-authentication/src/node/authProvider.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { AccountInfo, AuthenticationResult, ClientAuthError, ClientAuthErrorCodes, ServerError } from '@azure/msal-node'; -import { AuthenticationGetSessionOptions, AuthenticationProvider, AuthenticationProviderAuthenticationSessionsChangeEvent, AuthenticationProviderSessionOptions, AuthenticationSession, AuthenticationSessionAccountInformation, CancellationError, EventEmitter, ExtensionContext, ExtensionKind, l10n, LogOutputChannel, window } from 'vscode'; +import { AuthenticationGetSessionOptions, AuthenticationProvider, AuthenticationProviderAuthenticationSessionsChangeEvent, AuthenticationProviderSessionOptions, AuthenticationSession, AuthenticationSessionAccountInformation, CancellationError, EventEmitter, ExtensionContext, ExtensionKind, l10n, LogOutputChannel, Uri, window } from 'vscode'; import { Environment } from '@azure/ms-rest-azure-env'; import { CachedPublicClientApplicationManager } from './publicClientCache'; import { UriEventHandler } from '../UriEventHandler'; @@ -154,9 +154,9 @@ export class MsalAuthProvider implements AuthenticationProvider { //#region AuthenticationProvider methods - async getSessions(scopes: string[] | undefined, options?: AuthenticationGetSessionOptions): Promise { + async getSessions(scopes: string[] | undefined, options: AuthenticationGetSessionOptions = {}): Promise { const askingForAll = scopes === undefined; - const scopeData = new ScopeData(scopes); + const scopeData = new ScopeData(scopes, options?.issuer); // Do NOT use `scopes` beyond this place in the code. Use `scopeData` instead. this._logger.info('[getSessions]', askingForAll ? '[all]' : `[${scopeData.scopeStr}]`, 'starting'); @@ -186,7 +186,7 @@ export class MsalAuthProvider implements AuthenticationProvider { } async createSession(scopes: readonly string[], options: AuthenticationProviderSessionOptions): Promise { - const scopeData = new ScopeData(scopes); + const scopeData = new ScopeData(scopes, options.issuer); // Do NOT use `scopes` beyond this place in the code. Use `scopeData` instead. this._logger.info('[createSession]', `[${scopeData.scopeStr}]`, 'starting'); @@ -210,8 +210,9 @@ export class MsalAuthProvider implements AuthenticationProvider { } }; + const isNodeEnvironment = typeof process !== 'undefined' && typeof process?.versions?.node === 'string'; const flows = getMsalFlows({ - extensionHost: typeof navigator === 'undefined' + extensionHost: isNodeEnvironment ? this._context.extension.extensionKind === ExtensionKind.UI ? ExtensionHost.Local : ExtensionHost.Remote : ExtensionHost.WebWorker, }); diff --git a/extensions/microsoft-authentication/tsconfig.json b/extensions/microsoft-authentication/tsconfig.json index b40c2eb8716..dc4571cacda 100644 --- a/extensions/microsoft-authentication/tsconfig.json +++ b/extensions/microsoft-authentication/tsconfig.json @@ -23,6 +23,7 @@ "src/**/*", "../../src/vscode-dts/vscode.d.ts", "../../src/vscode-dts/vscode.proposed.idToken.d.ts", - "../../src/vscode-dts/vscode.proposed.nativeWindowHandle.d.ts" + "../../src/vscode-dts/vscode.proposed.nativeWindowHandle.d.ts", + "../../src/vscode-dts/vscode.proposed.authIssuers.d.ts" ] } diff --git a/extensions/notebook-renderers/src/index.ts b/extensions/notebook-renderers/src/index.ts index ebe5f26c147..dfff75d617e 100644 --- a/extensions/notebook-renderers/src/index.ts +++ b/extensions/notebook-renderers/src/index.ts @@ -16,7 +16,7 @@ function clearContainer(container: HTMLElement) { } function renderImage(outputInfo: OutputItem, element: HTMLElement): IDisposable { - const blob = new Blob([outputInfo.data()], { type: outputInfo.mime }); + const blob = new Blob([outputInfo.data() as Uint8Array], { type: outputInfo.mime }); const src = URL.createObjectURL(blob); const disposable = { dispose: () => { diff --git a/extensions/npm/package-lock.json b/extensions/npm/package-lock.json index 4ee7ee7a0e3..47f3164d798 100644 --- a/extensions/npm/package-lock.json +++ b/extensions/npm/package-lock.json @@ -20,7 +20,7 @@ }, "devDependencies": { "@types/minimatch": "^5.1.2", - "@types/node": "20.x", + "@types/node": "22.x", "@types/which": "^3.0.0" }, "engines": { @@ -34,12 +34,13 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.11.24", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.24.tgz", - "integrity": "sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long==", + "version": "22.13.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", + "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.20.0" } }, "node_modules/@types/which": { @@ -302,10 +303,11 @@ } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" }, "node_modules/vscode-uri": { "version": "3.0.8", diff --git a/extensions/npm/package.json b/extensions/npm/package.json index 847a4819465..de458a52069 100644 --- a/extensions/npm/package.json +++ b/extensions/npm/package.json @@ -32,7 +32,7 @@ }, "devDependencies": { "@types/minimatch": "^5.1.2", - "@types/node": "20.x", + "@types/node": "22.x", "@types/which": "^3.0.0" }, "main": "./out/npmMain", diff --git a/extensions/php-language-features/package-lock.json b/extensions/php-language-features/package-lock.json index 21fdb6b8e9f..b27fe3a2191 100644 --- a/extensions/php-language-features/package-lock.json +++ b/extensions/php-language-features/package-lock.json @@ -12,7 +12,7 @@ "which": "^2.0.2" }, "devDependencies": { - "@types/node": "20.x", + "@types/node": "22.x", "@types/which": "^2.0.0" }, "engines": { @@ -20,12 +20,13 @@ } }, "node_modules/@types/node": { - "version": "20.11.24", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.24.tgz", - "integrity": "sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long==", + "version": "22.13.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", + "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.20.0" } }, "node_modules/@types/which": { @@ -40,10 +41,11 @@ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" }, "node_modules/which": { "version": "2.0.2", diff --git a/extensions/php-language-features/package.json b/extensions/php-language-features/package.json index 989213b6c0c..8bb61e5b626 100644 --- a/extensions/php-language-features/package.json +++ b/extensions/php-language-features/package.json @@ -77,7 +77,7 @@ "which": "^2.0.2" }, "devDependencies": { - "@types/node": "20.x", + "@types/node": "22.x", "@types/which": "^2.0.0" }, "repository": { diff --git a/extensions/php-language-features/src/features/completionItemProvider.ts b/extensions/php-language-features/src/features/completionItemProvider.ts index ba82b71f606..ea52cf266e9 100644 --- a/extensions/php-language-features/src/features/completionItemProvider.ts +++ b/extensions/php-language-features/src/features/completionItemProvider.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CompletionItemProvider, CompletionItem, CompletionItemKind, CancellationToken, TextDocument, Position, Range, TextEdit, workspace, CompletionContext } from 'vscode'; -import * as phpGlobals from './phpGlobals'; +import { CancellationToken, CompletionContext, CompletionItem, CompletionItemKind, CompletionItemProvider, Position, Range, TextDocument, workspace } from 'vscode'; import * as phpGlobalFunctions from './phpGlobalFunctions'; +import * as phpGlobals from './phpGlobals'; export default class PHPCompletionItemProvider implements CompletionItemProvider { @@ -56,7 +56,8 @@ export default class PHPCompletionItemProvider implements CompletionItemProvider if (beforeWord === '>)", - "name": "keyword.operator.assignment.r" - }, - { - "match": "%(between|chin|do|dopar|in|like|\\+replace|\\+|:|T>|<>|>|\\$)%", - "name": "keyword.operator.other.r" - }, - { - "match": "\\.\\.\\.", - "name": "keyword.other.r" - }, - { - "match": ":::?", - "name": "punctuation.accessor.colons.r" - }, - { - "match": "(%%|\\*\\*)", - "name": "keyword.operator.arithmetic.r" - }, - { - "match": "(<-|->)", - "name": "keyword.operator.assignment.r" - }, - { - "match": "\\|>", - "name": "keyword.operator.assignment.redirection.r" - }, - { - "match": "(==|!=|<>|<=?|>=?)", - "name": "keyword.operator.comparison.r" - }, - { - "match": "(&&?|\\|\\|?)", - "name": "keyword.operator.logical.r" - }, - { - "match": ":=", - "name": "keyword.operator.other.r" - }, - { - "match": "[-+*/^]", - "name": "keyword.operator.arithmetic.r" - }, - { - "match": "=", - "name": "keyword.operator.assignment.r" - }, - { - "match": "!", - "name": "keyword.operator.logical.r" - }, - { - "match": "[:~@]", - "name": "keyword.other.r" - }, - { - "match": ";", - "name": "punctuation.terminator.semicolon.r" - } - ] - }, - "keywords": { - "patterns": [ - { - "match": "\\bif\\b(?=\\s*\\()", - "name": "keyword.control.conditional.if.r" - }, - { - "match": "\\belse\\b", - "name": "keyword.control.conditional.else.r" - }, - { - "match": "\\bbreak\\b", - "name": "keyword.control.flow.break.r" - }, - { - "match": "\\bnext\\b", - "name": "keyword.control.flow.continue.r" - }, - { - "match": "\\breturn(?=\\s*\\()", - "name": "keyword.control.flow.return.r" - }, - { - "match": "\\brepeat\\b", - "name": "keyword.control.loop.repeat.r" - }, - { - "match": "\\bfor\\b(?=\\s*\\()", - "name": "keyword.control.loop.for.r" - }, - { - "match": "\\bwhile\\b(?=\\s*\\()", - "name": "keyword.control.loop.while.r" - }, - { - "match": "\\bin\\b", - "name": "keyword.operator.word.r" - } - ] - }, - "storage-type": { - "patterns": [ - { - "begin": "\\b(character|complex|double|expression|integer|list|logical|numeric|single|raw|pairlist)\\b\\s*(\\()", - "beginCaptures": { - "1": { - "name": "storage.type.r" - }, - "2": { - "name": "punctuation.definition.arguments.begin.r" - } - }, - "contentName": "meta.function-call.arguments.r", - "end": "(\\))", - "endCaptures": { - "1": { - "name": "punctuation.definition.arguments.end.r" - } - }, - "name": "meta.function-call.r", - "patterns": [ - { - "include": "#function-call-arguments" - } - ] - } - ] - }, - "strings": { - "patterns": [ - { - "begin": "[rR]\"(-*)\\[", - "beginCaptures": { - "0": { - "name": "punctuation.definition.string.raw.begin.r" - } - }, - "end": "\\]\\1\"", - "endCaptures": { - "0": { - "name": "punctuation.definition.string.raw.end.r" - } - }, - "name": "string.quoted.double.raw.r" - }, - { - "begin": "[rR]'(-*)\\[", - "beginCaptures": { - "0": { - "name": "punctuation.definition.string.raw.begin.r" - } - }, - "end": "\\]\\1'", - "endCaptures": { - "0": { - "name": "punctuation.definition.string.raw.end.r" - } - }, - "name": "string.quoted.single.raw.r" - }, - { - "begin": "[rR]\"(-*)\\{", - "beginCaptures": { - "0": { - "name": "punctuation.definition.string.raw.begin.r" - } - }, - "end": "\\}\\1\"", - "endCaptures": { - "0": { - "name": "punctuation.definition.string.raw.end.r" - } - }, - "name": "string.quoted.double.raw.r" - }, - { - "begin": "[rR]'(-*)\\{", - "beginCaptures": { - "0": { - "name": "punctuation.definition.string.raw.begin.r" - } - }, - "end": "\\}\\1'", - "endCaptures": { - "0": { - "name": "punctuation.definition.string.raw.end.r" - } - }, - "name": "string.quoted.single.raw.r" - }, - { - "begin": "[rR]\"(-*)\\(", - "beginCaptures": { - "0": { - "name": "punctuation.definition.string.raw.begin.r" - } - }, - "end": "\\)\\1\"", - "endCaptures": { - "0": { - "name": "punctuation.definition.string.raw.end.r" - } - }, - "name": "string.quoted.double.raw.r" - }, - { - "begin": "[rR]'(-*)\\(", - "beginCaptures": { - "0": { - "name": "punctuation.definition.string.raw.begin.r" - } - }, - "end": "\\)\\1'", - "endCaptures": { - "0": { - "name": "punctuation.definition.string.raw.end.r" - } - }, - "name": "string.quoted.single.raw.r" - }, - { - "begin": "\"", - "beginCaptures": { - "0": { - "name": "punctuation.definition.string.begin.r" - } - }, - "end": "\"", - "endCaptures": { - "0": { - "name": "punctuation.definition.string.end.r" - } - }, - "name": "string.quoted.double.r", - "patterns": [ - { - "match": "\\\\.", - "name": "constant.character.escape.r" - } - ] - }, - { - "begin": "'", - "beginCaptures": { - "0": { - "name": "punctuation.definition.string.begin.r" - } - }, - "end": "'", - "endCaptures": { - "0": { - "name": "punctuation.definition.string.end.r" - } - }, - "name": "string.quoted.single.r", - "patterns": [ - { - "match": "\\\\.", - "name": "constant.character.escape.r" + "include": "#function-calls" } ] } @@ -427,13 +87,13 @@ "begin": "\\(", "beginCaptures": { "0": { - "name": "punctuation.section.parens.begin.r" + "name": "punctuation.section.parameters.begin.bracket.round.r" } }, "end": "\\)", "endCaptures": { "0": { - "name": "punctuation.section.parens.end.r" + "name": "punctuation.section.parameters.end.bracket.round.r" } }, "patterns": [ @@ -462,6 +122,7 @@ ] }, { + "contentName": "meta.item-access.arguments.r", "begin": "\\[\\[", "beginCaptures": { "0": { @@ -474,7 +135,6 @@ "name": "punctuation.section.brackets.double.end.r" } }, - "contentName": "meta.item-access.arguments.r", "patterns": [ { "include": "source.r" @@ -485,13 +145,13 @@ "begin": "\\{", "beginCaptures": { "0": { - "name": "punctuation.section.braces.begin.r" + "name": "punctuation.section.block.begin.bracket.curly.r" } }, "end": "\\}", "endCaptures": { "0": { - "name": "punctuation.section.braces.end.r" + "name": "punctuation.section.block.end.bracket.curly.r" } }, "patterns": [ @@ -502,9 +162,321 @@ } ] }, + "builtin-functions": { + "patterns": [ + { + "name": "meta.function-call.r", + "contentName": "meta.function-call.arguments.r", + "begin": "\\b(?:(base)(::))?(abbreviate|abs|acos|acosh|activeBindingFunction|addNA|addTaskCallback|agrep|agrepl|alist|all|all\\.equal|all\\.equal\\.character|all\\.equal\\.default|all\\.equal\\.environment|all\\.equal\\.envRefClass|all\\.equal\\.factor|all\\.equal\\.formula|all\\.equal\\.function|all\\.equal\\.language|all\\.equal\\.list|all\\.equal\\.numeric|all\\.equal\\.POSIXt|all\\.equal\\.raw|all\\.names|all\\.vars|allowInterrupts|any|anyDuplicated|anyDuplicated\\.array|anyDuplicated\\.data\\.frame|anyDuplicated\\.default|anyDuplicated\\.matrix|anyNA|anyNA\\.data\\.frame|anyNA\\.numeric_version|anyNA\\.POSIXlt|aperm|aperm\\.default|aperm\\.table|append|apply|Arg|args|array|array2DF|arrayInd|as\\.array|as\\.array\\.default|as\\.call|as\\.character|as\\.character\\.condition|as\\.character\\.Date|as\\.character\\.default|as\\.character\\.error|as\\.character\\.factor|as\\.character\\.hexmode|as\\.character\\.numeric_version|as\\.character\\.octmode|as\\.character\\.POSIXt|as\\.character\\.srcref|as\\.complex|as\\.data\\.frame|as\\.data\\.frame\\.array|as\\.data\\.frame\\.AsIs|as\\.data\\.frame\\.character|as\\.data\\.frame\\.complex|as\\.data\\.frame\\.data\\.frame|as\\.data\\.frame\\.Date|as\\.data\\.frame\\.default|as\\.data\\.frame\\.difftime|as\\.data\\.frame\\.factor|as\\.data\\.frame\\.integer|as\\.data\\.frame\\.list|as\\.data\\.frame\\.logical|as\\.data\\.frame\\.matrix|as\\.data\\.frame\\.model\\.matrix|as\\.data\\.frame\\.noquote|as\\.data\\.frame\\.numeric|as\\.data\\.frame\\.numeric_version|as\\.data\\.frame\\.ordered|as\\.data\\.frame\\.POSIXct|as\\.data\\.frame\\.POSIXlt|as\\.data\\.frame\\.raw|as\\.data\\.frame\\.table|as\\.data\\.frame\\.ts|as\\.data\\.frame\\.vector|as\\.Date|as\\.Date\\.character|as\\.Date\\.default|as\\.Date\\.factor|as\\.Date\\.numeric|as\\.Date\\.POSIXct|as\\.Date\\.POSIXlt|as\\.difftime|as\\.double|as\\.double\\.difftime|as\\.double\\.POSIXlt|as\\.environment|as\\.expression|as\\.expression\\.default|as\\.factor|as\\.function|as\\.function\\.default|as\\.hexmode|as\\.integer|as\\.list|as\\.list\\.data\\.frame|as\\.list\\.Date|as\\.list\\.default|as\\.list\\.difftime|as\\.list\\.environment|as\\.list\\.factor|as\\.list\\.function|as\\.list\\.numeric_version|as\\.list\\.POSIXct|as\\.list\\.POSIXlt|as\\.logical|as\\.logical\\.factor|as\\.matrix|as\\.matrix\\.data\\.frame|as\\.matrix\\.default|as\\.matrix\\.noquote|as\\.matrix\\.POSIXlt|as\\.name|as\\.null|as\\.null\\.default|as\\.numeric|as\\.numeric_version|as\\.octmode|as\\.ordered|as\\.package_version|as\\.pairlist|as\\.POSIXct|as\\.POSIXct\\.Date|as\\.POSIXct\\.default|as\\.POSIXct\\.numeric|as\\.POSIXct\\.POSIXlt|as\\.POSIXlt|as\\.POSIXlt\\.character|as\\.POSIXlt\\.Date|as\\.POSIXlt\\.default|as\\.POSIXlt\\.factor|as\\.POSIXlt\\.numeric|as\\.POSIXlt\\.POSIXct|as\\.qr|as\\.raw|as\\.single|as\\.single\\.default|as\\.symbol|as\\.table|as\\.table\\.default|as\\.vector|as\\.vector\\.data\\.frame|as\\.vector\\.factor|as\\.vector\\.POSIXlt|asin|asinh|asNamespace|asplit|asS3|asS4|assign|atan|atan2|atanh|attach|attachNamespace|attr|attr\\.all\\.equal|attributes|autoload|autoloader|backsolve|balancePOSIXlt|baseenv|basename|besselI|besselJ|besselK|besselY|beta|bindingIsActive|bindingIsLocked|bindtextdomain|bitwAnd|bitwNot|bitwOr|bitwShiftL|bitwShiftR|bitwXor|body|bquote|break|browser|browserCondition|browserSetDebug|browserText|builtins|by|by\\.data\\.frame|by\\.default|bzfile|c|c\\.Date|c\\.difftime|c\\.factor|c\\.noquote|c\\.numeric_version|c\\.POSIXct|c\\.POSIXlt|c\\.warnings|call|callCC|capabilities|casefold|cat|cbind|cbind\\.data\\.frame|ceiling|char\\.expand|character|charmatch|charToRaw|chartr|chkDots|chol|chol\\.default|chol2inv|choose|chooseOpsMethod|chooseOpsMethod\\.default|class|clearPushBack|close|close\\.connection|close\\.srcfile|close\\.srcfilealias|closeAllConnections|col|colMeans|colnames|colSums|commandArgs|comment|complex|computeRestarts|conditionCall|conditionCall\\.condition|conditionMessage|conditionMessage\\.condition|conflictRules|conflicts|Conj|contributors|cos|cosh|cospi|crossprod|Cstack_info|cummax|cummin|cumprod|cumsum|curlGetHeaders|cut|cut\\.Date|cut\\.default|cut\\.POSIXt|data\\.class|data\\.frame|data\\.matrix|date|debug|debuggingState|debugonce|declare|default\\.stringsAsFactors|delayedAssign|deparse|deparse1|det|detach|determinant|determinant\\.matrix|dget|diag|diff|diff\\.Date|diff\\.default|diff\\.difftime|diff\\.POSIXt|difftime|digamma|dim|dim\\.data\\.frame|dimnames|dimnames\\.data\\.frame|dir|dir\\.create|dir\\.exists|dirname|do\\.call|dontCheck|double|dput|dQuote|drop|droplevels|droplevels\\.data\\.frame|droplevels\\.factor|dump|duplicated|duplicated\\.array|duplicated\\.data\\.frame|duplicated\\.default|duplicated\\.matrix|duplicated\\.numeric_version|duplicated\\.POSIXlt|duplicated\\.warnings|dyn\\.load|dyn\\.unload|dynGet|eapply|eigen|emptyenv|enc2native|enc2utf8|encodeString|Encoding|endsWith|enquote|env\\.profile|environment|environmentIsLocked|environmentName|errorCondition|eval|eval\\.parent|evalq|Exec|exists|exp|expand\\.grid|expm1|expression|extSoftVersion|factor|factorial|fifo|file|file\\.access|file\\.append|file\\.choose|file\\.copy|file\\.create|file\\.exists|file\\.info|file\\.link|file\\.mode|file\\.mtime|file\\.path|file\\.remove|file\\.rename|file\\.show|file\\.size|file\\.symlink|Filter|Find|find\\.package|findInterval|findPackageEnv|findRestart|floor|flush|flush\\.connection|for|force|forceAndCall|formals|format|format\\.AsIs|format\\.data\\.frame|format\\.Date|format\\.default|format\\.difftime|format\\.factor|format\\.hexmode|format\\.info|format\\.libraryIQR|format\\.numeric_version|format\\.octmode|format\\.packageInfo|format\\.POSIXct|format\\.POSIXlt|format\\.pval|format\\.summaryDefault|formatC|formatDL|forwardsolve|function|gamma|gc|gc\\.time|gcinfo|gctorture|gctorture2|get|get0|getAllConnections|getCallingDLL|getCallingDLLe|getConnection|getDLLRegisteredRoutines|getDLLRegisteredRoutines\\.character|getDLLRegisteredRoutines\\.DLLInfo|getElement|geterrmessage|getExportedValue|getHook|getLoadedDLLs|getNamespace|getNamespaceExports|getNamespaceImports|getNamespaceInfo|getNamespaceName|getNamespaceUsers|getNamespaceVersion|getNativeSymbolInfo|getOption|getRversion|getSrcLines|getTaskCallbackNames|gettext|gettextf|getwd|gl|globalCallingHandlers|globalenv|gregexec|gregexpr|grep|grepl|grepRaw|grepv|grouping|gsub|gzcon|gzfile|I|iconv|iconvlist|icuGetCollate|icuSetCollate|identical|identity|if|ifelse|Im|importIntoEnv|infoRDS|inherits|integer|interaction|interactive|intersect|intToBits|intToUtf8|inverse\\.rle|invisible|invokeRestart|invokeRestartInteractively|is\\.array|is\\.atomic|is\\.call|is\\.character|is\\.complex|is\\.data\\.frame|is\\.double|is\\.element|is\\.environment|is\\.expression|is\\.factor|is\\.finite|is\\.finite\\.POSIXlt|is\\.function|is\\.infinite|is\\.infinite\\.POSIXlt|is\\.integer|is\\.language|is\\.list|is\\.loaded|is\\.logical|is\\.matrix|is\\.na|is\\.na\\.data\\.frame|is\\.na\\.numeric_version|is\\.na\\.POSIXlt|is\\.name|is\\.nan|is\\.nan\\.POSIXlt|is\\.null|is\\.numeric|is\\.numeric_version|is\\.numeric\\.Date|is\\.numeric\\.difftime|is\\.numeric\\.POSIXt|is\\.object|is\\.ordered|is\\.package_version|is\\.pairlist|is\\.primitive|is\\.qr|is\\.R|is\\.raw|is\\.recursive|is\\.single|is\\.symbol|is\\.table|is\\.unsorted|is\\.vector|isa|isatty|isBaseNamespace|isdebugged|isFALSE|isIncomplete|isNamespace|isNamespaceLoaded|ISOdate|ISOdatetime|isOpen|isRestart|isS4|isSeekable|isSymmetric|isSymmetric\\.matrix|isTRUE|jitter|julian|julian\\.Date|julian\\.POSIXt|kappa|kappa\\.default|kappa\\.lm|kappa\\.qr|kronecker|l10n_info|La_library|La_version|La\\.svd|labels|labels\\.default|lapply|lazyLoad|lazyLoadDBexec|lazyLoadDBfetch|lbeta|lchoose|length|length\\.POSIXlt|lengths|levels|levels\\.default|lfactorial|lgamma|libcurlVersion|library|library\\.dynam|library\\.dynam\\.unload|licence|license|list|list\\.dirs|list\\.files|list2DF|list2env|load|loadedNamespaces|loadingNamespaceInfo|loadNamespace|local|lockBinding|lockEnvironment|log|log10|log1p|log2|logb|logical|lower\\.tri|ls|make\\.names|make\\.unique|makeActiveBinding|Map|mapply|margin\\.table|marginSums|mat\\.or\\.vec|match|match\\.arg|match\\.call|match\\.fun|Math\\.data\\.frame|Math\\.Date|Math\\.difftime|Math\\.factor|Math\\.POSIXt|matrix|max|max\\.col|mean|mean\\.Date|mean\\.default|mean\\.difftime|mean\\.POSIXct|mean\\.POSIXlt|mem\\.maxNSize|mem\\.maxVSize|memCompress|memDecompress|memory\\.profile|merge|merge\\.data\\.frame|merge\\.default|message|mget|min|missing|Mod|mode|months|months\\.Date|months\\.POSIXt|mtfrm|mtfrm\\.default|mtfrm\\.POSIXct|mtfrm\\.POSIXlt|nameOfClass|nameOfClass\\.default|names|names\\.POSIXlt|namespaceExport|namespaceImport|namespaceImportClasses|namespaceImportFrom|namespaceImportMethods|nargs|nchar|ncol|NCOL|Negate|new\\.env|next|NextMethod|ngettext|nlevels|noquote|norm|normalizePath|nrow|NROW|nullfile|numeric|numeric_version|numToBits|numToInts|nzchar|objects|oldClass|OlsonNames|on\\.exit|open|open\\.connection|open\\.srcfile|open\\.srcfilealias|open\\.srcfilecopy|Ops\\.data\\.frame|Ops\\.Date|Ops\\.difftime|Ops\\.factor|Ops\\.numeric_version|Ops\\.ordered|Ops\\.POSIXt|options|order|ordered|outer|package_version|packageEvent|packageHasNamespace|packageNotFoundError|packageStartupMessage|packBits|pairlist|parent\\.env|parent\\.frame|parse|parseNamespaceFile|paste|paste0|path\\.expand|path\\.package|pcre_config|pipe|plot|pmatch|pmax|pmax\\.int|pmin|pmin\\.int|polyroot|pos\\.to\\.env|Position|pretty|pretty\\.default|prettyNum|print|print\\.AsIs|print\\.by|print\\.condition|print\\.connection|print\\.data\\.frame|print\\.Date|print\\.default|print\\.difftime|print\\.Dlist|print\\.DLLInfo|print\\.DLLInfoList|print\\.DLLRegisteredRoutines|print\\.eigen|print\\.factor|print\\.function|print\\.hexmode|print\\.libraryIQR|print\\.listof|print\\.NativeRoutineList|print\\.noquote|print\\.numeric_version|print\\.octmode|print\\.packageInfo|print\\.POSIXct|print\\.POSIXlt|print\\.proc_time|print\\.restart|print\\.rle|print\\.simple\\.list|print\\.srcfile|print\\.srcref|print\\.summary\\.table|print\\.summary\\.warnings|print\\.summaryDefault|print\\.table|print\\.warnings|prmatrix|proc\\.time|prod|prop\\.table|proportions|provideDimnames|psigamma|pushBack|pushBackLength|q|qr|qr\\.coef|qr\\.default|qr\\.fitted|qr\\.Q|qr\\.qty|qr\\.qy|qr\\.R|qr\\.resid|qr\\.solve|qr\\.X|quarters|quarters\\.Date|quarters\\.POSIXt|quit|quote|R_compiled_by|R_system_version|R\\.home|R\\.Version|range|range\\.Date|range\\.default|range\\.POSIXct|rank|rapply|raw|rawConnection|rawConnectionValue|rawShift|rawToBits|rawToChar|rbind|rbind\\.data\\.frame|rcond|Re|read\\.dcf|readBin|readChar|readline|readLines|readRDS|readRenviron|Recall|Reduce|reg\\.finalizer|regexec|regexpr|registerS3method|registerS3methods|regmatches|remove|removeTaskCallback|rep|rep_len|rep\\.Date|rep\\.difftime|rep\\.factor|rep\\.int|rep\\.numeric_version|rep\\.POSIXct|rep\\.POSIXlt|repeat|replace|replicate|require|requireNamespace|restartDescription|restartFormals|retracemem|return|returnValue|rev|rev\\.default|rle|rm|RNGkind|RNGversion|round|round\\.Date|round\\.POSIXt|row|row\\.names|row\\.names\\.data\\.frame|row\\.names\\.default|rowMeans|rownames|rowsum|rowsum\\.data\\.frame|rowsum\\.default|rowSums|sample|sample\\.int|sapply|save|save\\.image|saveRDS|scale|scale\\.default|scan|search|searchpaths|seek|seek\\.connection|seq|seq_along|seq_len|seq\\.Date|seq\\.default|seq\\.int|seq\\.POSIXt|sequence|sequence\\.default|serialize|serverSocket|set\\.seed|setdiff|setequal|setHook|setNamespaceInfo|setSessionTimeLimit|setTimeLimit|setwd|showConnections|shQuote|sign|signalCondition|signif|simpleCondition|simpleError|simpleMessage|simpleWarning|simplify2array|sin|single|sinh|sink|sink\\.number|sinpi|slice\\.index|socketAccept|socketConnection|socketSelect|socketTimeout|solve|solve\\.default|solve\\.qr|sort|sort_by|sort_by\\.data\\.frame|sort_by\\.default|sort\\.default|sort\\.int|sort\\.list|sort\\.POSIXlt|source|split|split\\.data\\.frame|split\\.Date|split\\.default|split\\.POSIXct|sprintf|sqrt|sQuote|srcfile|srcfilealias|srcfilecopy|srcref|standardGeneric|startsWith|stderr|stdin|stdout|stop|stopifnot|storage\\.mode|str2expression|str2lang|strftime|strptime|strrep|strsplit|strtoi|strtrim|structure|strwrap|sub|subset|subset\\.data\\.frame|subset\\.default|subset\\.matrix|substitute|substr|substring|sum|summary|summary\\.connection|summary\\.data\\.frame|Summary\\.data\\.frame|summary\\.Date|Summary\\.Date|summary\\.default|summary\\.difftime|Summary\\.difftime|summary\\.factor|Summary\\.factor|summary\\.matrix|Summary\\.numeric_version|Summary\\.ordered|summary\\.POSIXct|Summary\\.POSIXct|summary\\.POSIXlt|Summary\\.POSIXlt|summary\\.proc_time|summary\\.srcfile|summary\\.srcref|summary\\.table|summary\\.warnings|suppressMessages|suppressPackageStartupMessages|suppressWarnings|suspendInterrupts|svd|sweep|switch|sys\\.call|sys\\.calls|Sys\\.chmod|Sys\\.Date|sys\\.frame|sys\\.frames|sys\\.function|Sys\\.getenv|Sys\\.getlocale|Sys\\.getpid|Sys\\.glob|Sys\\.info|sys\\.load\\.image|Sys\\.localeconv|sys\\.nframe|sys\\.on\\.exit|sys\\.parent|sys\\.parents|Sys\\.readlink|sys\\.save\\.image|Sys\\.setenv|Sys\\.setFileTime|Sys\\.setLanguage|Sys\\.setlocale|Sys\\.sleep|sys\\.source|sys\\.status|Sys\\.time|Sys\\.timezone|Sys\\.umask|Sys\\.unsetenv|Sys\\.which|system|system\\.file|system\\.time|system2|t|t\\.data\\.frame|t\\.default|table|tabulate|Tailcall|tan|tanh|tanpi|tapply|taskCallbackManager|tcrossprod|tempdir|tempfile|textConnection|textConnectionValue|tolower|topenv|toString|toString\\.default|toupper|trace|traceback|tracemem|tracingState|transform|transform\\.data\\.frame|transform\\.default|trigamma|trimws|trunc|trunc\\.Date|trunc\\.POSIXt|truncate|truncate\\.connection|try|tryCatch|tryInvokeRestart|typeof|unCfillPOSIXlt|unclass|undebug|union|unique|unique\\.array|unique\\.data\\.frame|unique\\.default|unique\\.matrix|unique\\.numeric_version|unique\\.POSIXlt|unique\\.warnings|units|units\\.difftime|unix\\.time|unlink|unlist|unloadNamespace|unlockBinding|unname|unserialize|unsplit|untrace|untracemem|unz|upper\\.tri|url|use|UseMethod|utf8ToInt|validEnc|validUTF8|vapply|vector|Vectorize|warning|warningCondition|warnings|weekdays|weekdays\\.Date|weekdays\\.POSIXt|which|which\\.max|which\\.min|while|with|with\\.default|withAutoprint|withCallingHandlers|within|within\\.data\\.frame|within\\.list|withRestarts|withVisible|write|write\\.dcf|writeBin|writeChar|writeLines|xor|xpdrows\\.data\\.frame|xtfrm|xtfrm\\.AsIs|xtfrm\\.data\\.frame|xtfrm\\.Date|xtfrm\\.default|xtfrm\\.difftime|xtfrm\\.factor|xtfrm\\.numeric_version|xtfrm\\.POSIXct|xtfrm\\.POSIXlt|xzfile|zapsmall|zstdfile)\\s*(\\()", + "beginCaptures": { + "1": { + "name": "support.namespace.r" + }, + "2": { + "name": "punctuation.accessor.colons.r" + }, + "3": { + "name": "support.function.r" + }, + "4": { + "name": "punctuation.definition.arguments.begin.r" + } + }, + "end": "(\\))", + "endCaptures": { + "1": { + "name": "punctuation.definition.arguments.end.r" + } + }, + "patterns": [ + { + "include": "#function-call-arguments" + } + ] + }, + { + "name": "meta.function-call.r", + "contentName": "meta.function-call.arguments.r", + "begin": "\\b(?:(graphics)(::))?(abline|arrows|assocplot|axis|Axis|axis\\.Date|Axis\\.Date|Axis\\.default|axis\\.POSIXct|Axis\\.POSIXt|Axis\\.table|axTicks|barplot|barplot\\.default|barplot\\.formula|box|boxplot|boxplot\\.default|boxplot\\.formula|boxplot\\.matrix|bxp|cdplot|cdplot\\.default|cdplot\\.formula|clip|close\\.screen|co\\.intervals|contour|contour\\.default|coplot|curve|dotchart|erase\\.screen|extendDateTimeFormat|filled\\.contour|fourfoldplot|frame|grconvertX|grconvertY|grid|hist|hist\\.Date|hist\\.default|hist\\.POSIXt|identify|identify\\.default|image|image\\.default|layout|layout\\.show|lcm|legend|lines|lines\\.default|lines\\.formula|lines\\.histogram|lines\\.table|locator|matlines|matplot|matpoints|mosaicplot|mosaicplot\\.default|mosaicplot\\.formula|mtext|pairs|pairs\\.default|pairs\\.formula|panel\\.smooth|par|persp|persp\\.default|pie|piechart|plot\\.data\\.frame|plot\\.default|plot\\.design|plot\\.factor|plot\\.formula|plot\\.function|plot\\.histogram|plot\\.new|plot\\.raster|plot\\.table|plot\\.window|plot\\.xy|plotHclust|points|points\\.default|points\\.formula|points\\.table|polygon|polypath|rasterImage|rect|rug|screen|segments|smoothScatter|spineplot|spineplot\\.default|spineplot\\.formula|split\\.screen|stars|stem|strheight|stripchart|stripchart\\.default|stripchart\\.formula|strwidth|sunflowerplot|sunflowerplot\\.default|sunflowerplot\\.formula|symbols|text|text\\.default|text\\.formula|title|xinch|xspline|xyinch|yinch)\\s*(\\()", + "beginCaptures": { + "1": { + "name": "support.namespace.r" + }, + "2": { + "name": "punctuation.accessor.colons.r" + }, + "3": { + "name": "support.function.r" + }, + "4": { + "name": "punctuation.definition.arguments.begin.r" + } + }, + "end": "(\\))", + "endCaptures": { + "1": { + "name": "punctuation.definition.arguments.end.r" + } + }, + "patterns": [ + { + "include": "#function-call-arguments" + } + ] + }, + { + "name": "meta.function-call.r", + "contentName": "meta.function-call.arguments.r", + "begin": "\\b(?:(grDevices)(::))?(adjustcolor|anyNA\\.raster|as\\.graphicsAnnot|as\\.matrix\\.raster|as\\.raster|as\\.raster\\.array|as\\.raster\\.character|as\\.raster\\.logical|as\\.raster\\.matrix|as\\.raster\\.numeric|as\\.raster\\.raster|as\\.raster\\.raw|axisTicks|bitmap|bmp|boxplot\\.stats|c2to3|cairo_pdf|cairo_ps|cairoFT|cairoSymbolFont|cairoVersion|check_gs_type|check\\.options|checkFont|checkFont\\.CIDFont|checkFont\\.default|checkFont\\.Type1Font|checkFontInUse|checkIntFormat|checkQuartzFont|checkSymbolFont|checkX11Font|chromaticAdaptation|chull|CIDFont|cm|cm\\.colors|col2rgb|colorConverter|colorRamp|colorRampPalette|colors|colours|comparePangoVersion|contourLines|convertColor|densCols|dev\\.capabilities|dev\\.capture|dev\\.control|dev\\.copy|dev\\.copy2eps|dev\\.copy2pdf|dev\\.cur|dev\\.displaylist|dev\\.flush|dev\\.hold|dev\\.interactive|dev\\.list|dev\\.new|dev\\.next|dev\\.off|dev\\.prev|dev\\.print|dev\\.set|dev\\.size|dev2bitmap|devAskNewPage|deviceIsInteractive|embedFonts|embedGlyphs|extendrange|getGraphicsEvent|getGraphicsEventEnv|glyphAnchor|glyphFont|glyphFontList|glyphHeight|glyphHeightBottom|glyphInfo|glyphJust|glyphJust\\.character|glyphJust\\.GlyphJust|glyphJust\\.numeric|glyphWidth|glyphWidthLeft|graphics\\.off|gray|gray\\.colors|grey|grey\\.colors|grSoftVersion|guessEncoding|hcl|hcl\\.colors|hcl\\.pals|heat\\.colors|hsv|initPSandPDFfonts|invertStyle|is\\.na\\.raster|is\\.raster|isPDF|jpeg|make\\.rgb|mapCharWeight|mapStyle|mapWeight|matchEncoding|matchEncoding\\.CIDFont|matchEncoding\\.Type1Font|matchFont|n2mfrow|nclass\\.FD|nclass\\.scott|nclass\\.Sturges|Ops\\.raster|optionSymbolFont|palette|palette\\.colors|palette\\.match|palette\\.pals|pangoVersion|pattern|pdf|pdf\\.options|pdfFonts|pictex|png|postscript|postscriptFonts|pow3|prettyDate|print\\.colorConverter|print\\.raster|print\\.recordedplot|print\\.RGBcolorConverter|print\\.RGlyphFont|printFont|printFont\\.CIDFont|printFont\\.Type1Font|printFonts|ps\\.options|quartz|quartz\\.options|quartz\\.save|quartzFont|quartzFonts|rainbow|recordGraphics|recordPalette|recordPlot|replayPlot|restoreRecordedPlot|rgb|rgb2hsv|RGBcolorConverter|savePlot|seqDtime|setEPS|setFonts|setGraphicsEventEnv|setGraphicsEventHandlers|setPS|setQuartzFonts|setX11Fonts|svg|symbolfamilyDefault|symbolType1support|terrain\\.colors|tiff|topo\\.colors|trans3d|trunc_POSIXt|Type1Font|vectorizeConverter|warnLogCoords|x11|X11|X11\\.options|X11Font|X11FontError|X11Fonts|xfig|xy\\.coords|xyTable|xyz\\.coords)\\s*(\\()", + "beginCaptures": { + "1": { + "name": "support.namespace.r" + }, + "2": { + "name": "punctuation.accessor.colons.r" + }, + "3": { + "name": "support.function.r" + }, + "4": { + "name": "punctuation.definition.arguments.begin.r" + } + }, + "end": "(\\))", + "endCaptures": { + "1": { + "name": "punctuation.definition.arguments.end.r" + } + }, + "patterns": [ + { + "include": "#function-call-arguments" + } + ] + }, + { + "name": "meta.function-call.r", + "contentName": "meta.function-call.arguments.r", + "begin": "\\b(?:(methods)(::))?(addNextMethod|allNames|Arith|as|asMethodDefinition|assignClassDef|assignMethodsMetaData|balanceMethodsList|bind_activation|cacheGenericsMetaData|cacheMetaData|cacheMethod|cacheOnAssign|callGeneric|callNextMethod|canCoerce|cbind|cbind2|checkAtAssignment|checkSlotAssignment|classesToAM|classGeneratorFunction|classLabel|classMetaName|className|coerce|Compare|completeClassDefinition|completeExtends|completeSubclasses|Complex|conformMethod|defaultDumpName|defaultPrototype|dispatchIsInternal|doPrimitiveMethod|dumpMethod|dumpMethods|el|elNamed|empty\\.dump|emptyMethodsList|envRefInferField|envRefSetField|evalOnLoad|evalqOnLoad|evalSource|existsFunction|existsMethod|extends|externalRefMethod|finalDefaultMethod|findClass|findFunction|findMethod|findMethods|findMethodSignatures|findUnique|fixPre1\\.8|formalArgs|fromNextMethod|functionBody|generic\\.skeleton|genericForBasic|getAllSuperClasses|getClass|getClassDef|getClasses|getDataPart|getFunction|getGeneric|getGenericFromCall|getGenerics|getGroup|getGroupMembers|getLoadActions|getMethod|getMethods|getMethodsAndAccessors|getMethodsForDispatch|getMethodsMetaData|getPackageName|getRefClass|getRefSuperClasses|getSlots|getValidity|hasArg|hasLoadAction|hasMethod|hasMethods|implicitGeneric|inBasicFuns|inferProperties|inheritedSlotNames|inheritedSubMethodLists|initFieldArgs|initialize|initMethodDispatch|initRefFields|insertClassMethods|insertMethod|insertMethodInEmptyList|insertSource|installClassMethod|is|isBaseFun|isClass|isClassDef|isClassUnion|isGeneric|isGrammarSymbol|isGroup|isMixin|isRematched|isS3Generic|isSealedClass|isSealedMethod|isVirtualClass|isXS3Class|kronecker|languageEl|listFromMethods|loadMethod|Logic|makeClassMethod|makeClassRepresentation|makeEnvRefMethods|makeExtends|makeGeneric|makeMethodsList|makePrototypeFromClassDef|makeStandardGeneric|matchDefaults|matchSignature|Math|Math2|matrixOps|mergeMethods|metaNameUndo|method\\.skeleton|MethodAddCoerce|methodSignatureMatrix|MethodsList|MethodsListSelect|methodsPackageMetaName|missingArg|multipleClasses|new|newBasic|newClassRepresentation|newEmptyObject|Ops|outerLabels|packageSlot|possibleExtends|printClassRepresentation|printPropertiesList|prohibitGeneric|promptClass|promptMethods|prototype|Quote|rbind|rbind2|reconcilePropertiesAndPrototype|refClassFields|refClassInformation|refClassMethods|refClassPrompt|refObjectClass|registerImplicitGenerics|rematchDefinition|removeClass|removeGeneric|removeMethod|removeMethods|representation|requireMethods|resetClass|resetGeneric|S3Class|S3forS4Methods|S3Part|sealClass|selectMethod|selectSuperClasses|setAs|setCacheOnAssign|setClass|setClassUnion|setDataPart|setGeneric|setGenericImplicit|setGroupGeneric|setIs|setLoadAction|setLoadActions|setMethod|setNames|setOldClass|setPackageName|setPackageSlot|setPrimitiveMethods|setRefClass|setReplaceMethod|setValidity|show|showClass|showClassMethod|showDefault|showExtends|showExtraSlots|showMethods|showRefClassDef|signature|SignatureMethod|sigToEnv|slot|slotNames|slotsFromS3|substituteDirect|substituteFunctionArgs|Summary|superClassDepth|superClassMethodName|tableNames|testInheritedMethods|testVirtual|tryNew|unRematchDefinition|useMTable|validObject|validSlotNames)\\s*(\\()", + "beginCaptures": { + "1": { + "name": "support.namespace.r" + }, + "2": { + "name": "punctuation.accessor.colons.r" + }, + "3": { + "name": "support.function.r" + }, + "4": { + "name": "punctuation.definition.arguments.begin.r" + } + }, + "end": "(\\))", + "endCaptures": { + "1": { + "name": "punctuation.definition.arguments.end.r" + } + }, + "patterns": [ + { + "include": "#function-call-arguments" + } + ] + }, + { + "name": "meta.function-call.r", + "contentName": "meta.function-call.arguments.r", + "begin": "\\b(?:(stats)(::))?(acf|acf2AR|add\\.scope|add1|add1\\.default|add1\\.glm|add1\\.lm|add1\\.mlm|addmargins|aggregate|aggregate\\.data\\.frame|aggregate\\.default|aggregate\\.formula|aggregate\\.ts|AIC|AIC\\.default|AIC\\.logLik|alias|alias\\.formula|alias\\.lm|anova|anova\\.glm|anova\\.glmlist|anova\\.lm|anova\\.lmlist|anova\\.loess|anova\\.mlm|anova\\.mlmlist|anova\\.nls|anovalist\\.nls|ansari\\.test|ansari\\.test\\.default|ansari\\.test\\.formula|aov|approx|approxfun|ar|ar\\.burg|ar\\.burg\\.default|ar\\.burg\\.mts|ar\\.mle|ar\\.ols|ar\\.yw|ar\\.yw\\.default|ar\\.yw\\.mts|arima|arima\\.sim|arima0|arima0\\.diag|ARMAacf|ARMAtoMA|as\\.data\\.frame\\.aovproj|as\\.data\\.frame\\.ftable|as\\.data\\.frame\\.logLik|as\\.dendrogram|as\\.dendrogram\\.dendrogram|as\\.dendrogram\\.hclust|as\\.dist|as\\.dist\\.default|as\\.formula|as\\.hclust|as\\.hclust\\.default|as\\.hclust\\.dendrogram|as\\.hclust\\.twins|as\\.matrix\\.dist|as\\.matrix\\.ftable|as\\.stepfun|as\\.stepfun\\.default|as\\.stepfun\\.isoreg|as\\.table\\.ftable|as\\.ts|as\\.ts\\.default|asOneSidedFormula|assert_NULL_or_prob|ave|bandwidth\\.kernel|bartlett\\.test|bartlett\\.test\\.default|bartlett\\.test\\.formula|BIC|BIC\\.default|BIC\\.logLik|binom\\.test|binomial|binomInitialize|biplot|biplot\\.default|biplot\\.prcomp|biplot\\.princomp|Box\\.test|bw_pair_cnts|bw\\.bcv|bw\\.nrd|bw\\.nrd0|bw\\.SJ|bw\\.ucv|C|cancor|case\\.names|case\\.names\\.default|case\\.names\\.lm|cbind\\.ts|ccf|check_exact|chisq\\.test|cmdscale|coef|coef\\.aov|coef\\.Arima|coef\\.default|coef\\.listof|coef\\.maov|coef\\.nls|coefficients|complete\\.cases|confint|confint\\.default|confint\\.glm|confint\\.lm|confint\\.nls|confint\\.profile\\.glm|confint\\.profile\\.nls|constrOptim|contr\\.helmert|contr\\.poly|contr\\.SAS|contr\\.sum|contr\\.treatment|contrasts|convolve|cooks\\.distance|cooks\\.distance\\.glm|cooks\\.distance\\.lm|cophenetic|cophenetic\\.default|cophenetic\\.dendrogram|cor|cor\\.test|cor\\.test\\.default|cor\\.test\\.formula|cov|cov\\.wt|cov2cor|covratio|cpgram|cut\\.dendrogram|cutree|cycle|cycle\\.default|cycle\\.ts|D|dbeta|dbinom|dcauchy|dchisq|decompose|delete\\.response|deltat|deltat\\.default|dendrapply|density|density\\.default|deparse2|deriv|deriv\\.default|deriv\\.formula|deriv3|deriv3\\.default|deriv3\\.formula|deviance|deviance\\.default|deviance\\.glm|deviance\\.lm|deviance\\.mlm|deviance\\.nls|dexp|df|df\\.kernel|df\\.residual|df\\.residual\\.default|df\\.residual\\.nls|DF2formula|dfbeta|dfbeta\\.lm|dfbetas|dfbetas\\.lm|dffits|dgamma|dgeom|dhyper|diff\\.ts|diffinv|diffinv\\.default|diffinv\\.ts|diffinv\\.vector|dist|dlnorm|dlogis|dmultinom|dnbinom|dnorm|dpois|drop\\.scope|drop\\.terms|drop1|drop1\\.default|drop1\\.glm|drop1\\.lm|drop1\\.mlm|dsignrank|dt|dummy\\.coef|dummy\\.coef\\.aovlist|dummy\\.coef\\.lm|dunif|dweibull|dwilcox|ecdf|eff\\.aovlist|effects|effects\\.glm|effects\\.lm|embed|end|end\\.default|estVar|estVar\\.mlm|estVar\\.SSD|expand\\.model\\.frame|extractAIC|extractAIC\\.aov|extractAIC\\.coxph|extractAIC\\.glm|extractAIC\\.lm|extractAIC\\.negbin|extractAIC\\.survreg|factanal|factanal\\.fit\\.mle|factor\\.scope|family|family\\.glm|family\\.lm|fft|filter|fisher\\.test|fitted|fitted\\.default|fitted\\.isoreg|fitted\\.kmeans|fitted\\.nls|fitted\\.smooth\\.spline|fitted\\.values|fivenum|fligner\\.test|fligner\\.test\\.default|fligner\\.test\\.formula|format_perc|format\\.dist|format\\.ftable|formula|formula\\.character|formula\\.data\\.frame|formula\\.default|formula\\.formula|formula\\.glm|formula\\.lm|formula\\.nls|formula\\.terms|frequency|frequency\\.default|friedman\\.test|friedman\\.test\\.default|friedman\\.test\\.formula|ftable|ftable\\.default|ftable\\.formula|Gamma|gaussian|get_all_vars|getCall|getCall\\.default|getInitial|getInitial\\.default|getInitial\\.formula|getInitial\\.selfStart|glm|glm\\.control|glm\\.fit|hasTsp|hat|hatvalues|hatvalues\\.lm|hatvalues\\.smooth\\.spline|hclust|head\\.ts|heatmap|HL|HoltWinters|hyman_filter|identify\\.hclust|influence|influence\\.glm|influence\\.lm|influence\\.measures|integrate|interaction\\.plot|inverse\\.gaussian|IQR|is\\.empty\\.model|is\\.leaf|is\\.mts|is\\.stepfun|is\\.ts|is\\.tskernel|isoreg|KalmanForecast|KalmanLike|KalmanRun|KalmanSmooth|kernapply|kernapply\\.default|kernapply\\.ts|kernapply\\.tskernel|kernapply\\.vector|kernel|kmeans|knots|knots\\.stepfun|kruskal\\.test|kruskal\\.test\\.default|kruskal\\.test\\.formula|ks\\.test|ks\\.test\\.default|ks\\.test\\.formula|ksmooth|labels\\.dendrogram|labels\\.dist|labels\\.lm|labels\\.terms|lag|lag\\.default|lag\\.plot|line|lines\\.isoreg|lines\\.stepfun|lines\\.ts|lm|lm\\.fit|lm\\.influence|lm\\.wfit|loadings|loess|loess\\.control|loess\\.smooth|logLik|logLik\\.Arima|logLik\\.glm|logLik\\.lm|logLik\\.logLik|logLik\\.nls|loglin|lowess|ls\\.diag|ls\\.print|lsfit|mad|mahalanobis|make\\.link|make\\.tables\\.aovproj|make\\.tables\\.aovprojlist|makeARIMA|makepredictcall|makepredictcall\\.default|makepredictcall\\.poly|manova|mantelhaen\\.test|mauchly\\.test|mauchly\\.test\\.mlm|mauchly\\.test\\.SSD|mcnemar\\.test|median|median\\.default|medpolish|merge\\.dendrogram|midcache\\.dendrogram|model\\.extract|model\\.frame|model\\.frame\\.aovlist|model\\.frame\\.default|model\\.frame\\.glm|model\\.frame\\.lm|model\\.matrix|model\\.matrix\\.default|model\\.matrix\\.lm|model\\.offset|model\\.response|model\\.tables|model\\.tables\\.aov|model\\.tables\\.aovlist|model\\.weights|monthplot|monthplot\\.default|monthplot\\.stl|monthplot\\.StructTS|monthplot\\.ts|mood\\.test|mood\\.test\\.default|mood\\.test\\.formula|mvfft|na\\.action|na\\.action\\.default|na\\.contiguous|na\\.contiguous\\.default|na\\.exclude|na\\.exclude\\.data\\.frame|na\\.exclude\\.default|na\\.fail|na\\.fail\\.default|na\\.omit|na\\.omit\\.data\\.frame|na\\.omit\\.default|na\\.omit\\.ts|na\\.pass|napredict|napredict\\.default|napredict\\.exclude|naprint|naprint\\.default|naprint\\.exclude|naprint\\.omit|naresid|naresid\\.default|naresid\\.exclude|nextn|nleaves|nlm|nlminb|nls|nls_port_fit|nls\\.control|nlsModel|nlsModel\\.plinear|NLSstAsymptotic|NLSstAsymptotic\\.sortedXyData|NLSstClosestX|NLSstClosestX\\.sortedXyData|NLSstLfAsymptote|NLSstLfAsymptote\\.sortedXyData|NLSstRtAsymptote|NLSstRtAsymptote\\.sortedXyData|nobs|nobs\\.default|nobs\\.dendrogram|nobs\\.glm|nobs\\.lm|nobs\\.logLik|nobs\\.nls|numericDeriv|offset|oneway\\.test|Ops\\.ts|optim|optimHess|optimise|optimize|order\\.dendrogram|p\\.adjust|pacf|pacf\\.default|Pair|pairs\\.profile|pairwise\\.prop\\.test|pairwise\\.t\\.test|pairwise\\.table|pairwise\\.wilcox\\.test|pbeta|pbinom|pbirthday|pcauchy|pchisq|pexp|pf|pgamma|pgeom|phyper|Pillai|pkolmogorov|pkolmogorov_one_asymp|pkolmogorov_one_exact|pkolmogorov_two_asymp|pkolmogorov_two_exact|plclust|plnorm|plogis|plot\\.acf|plot\\.decomposed\\.ts|plot\\.dendrogram|plot\\.density|plot\\.ecdf|plot\\.hclust|plot\\.HoltWinters|plot\\.isoreg|plot\\.lm|plot\\.medpolish|plot\\.mlm|plot\\.ppr|plot\\.prcomp|plot\\.princomp|plot\\.profile|plot\\.profile\\.nls|plot\\.spec|plot\\.spec\\.coherency|plot\\.spec\\.phase|plot\\.stepfun|plot\\.stl|plot\\.ts|plot\\.tskernel|plot\\.TukeyHSD|plotNode|plotNodeLimit|pnbinom|pnorm|pointwise|poisson|poisson\\.test|poly|polym|port_get_named_v|port_msg|power|power\\.anova\\.test|power\\.prop\\.test|power\\.t\\.test|PP\\.test|ppoints|ppois|ppr|ppr\\.default|ppr\\.formula|prcomp|prcomp\\.default|prcomp\\.formula|predict|predict\\.ar|predict\\.Arima|predict\\.arima0|predict\\.glm|predict\\.HoltWinters|predict\\.lm|predict\\.loess|predict\\.mlm|predict\\.nls|predict\\.poly|predict\\.ppr|predict\\.prcomp|predict\\.princomp|predict\\.smooth\\.spline|predict\\.smooth\\.spline\\.fit|predict\\.StructTS|predLoess|preplot|princomp|princomp\\.default|princomp\\.formula|print\\.acf|print\\.anova|print\\.aov|print\\.aovlist|print\\.ar|print\\.Arima|print\\.arima0|print\\.dendrogram|print\\.density|print\\.dist|print\\.dummy_coef|print\\.dummy_coef_list|print\\.ecdf|print\\.factanal|print\\.family|print\\.formula|print\\.ftable|print\\.glm|print\\.hclust|print\\.HoltWinters|print\\.htest|print\\.infl|print\\.integrate|print\\.isoreg|print\\.kmeans|print\\.lm|print\\.loadings|print\\.loess|print\\.logLik|print\\.medpolish|print\\.mtable|print\\.nls|print\\.pairwise\\.htest|print\\.power\\.htest|print\\.ppr|print\\.prcomp|print\\.princomp|print\\.smooth\\.spline|print\\.stepfun|print\\.stl|print\\.StructTS|print\\.summary\\.aov|print\\.summary\\.aovlist|print\\.summary\\.ecdf|print\\.summary\\.glm|print\\.summary\\.lm|print\\.summary\\.loess|print\\.summary\\.manova|print\\.summary\\.nls|print\\.summary\\.ppr|print\\.summary\\.prcomp|print\\.summary\\.princomp|print\\.tables_aov|print\\.terms|print\\.ts|print\\.tskernel|print\\.TukeyHSD|print\\.tukeyline|print\\.tukeysmooth|print\\.xtabs|printCoefmat|profile|profile\\.glm|profile\\.nls|profiler|profiler\\.nls|proj|Proj|proj\\.aov|proj\\.aovlist|proj\\.default|proj\\.lm|promax|prop\\.test|prop\\.trend\\.test|psignrank|psmirnov|psmirnov_asymp|psmirnov_exact|psmirnov_simul|pt|ptukey|punif|pweibull|pwilcox|qbeta|qbinom|qbirthday|qcauchy|qchisq|qexp|qf|qgamma|qgeom|qhyper|qlnorm|qlogis|qnbinom|qnorm|qpois|qqline|qqnorm|qqnorm\\.default|qqplot|qr\\.influence|qr\\.lm|qsignrank|qsmirnov|qt|qtukey|quade\\.test|quade\\.test\\.default|quade\\.test\\.formula|quantile|quantile\\.default|quantile\\.ecdf|quantile\\.POSIXt|quasi|quasibinomial|quasipoisson|qunif|qweibull|qwilcox|r2dtable|Rank|rbeta|rbinom|rcauchy|rchisq|read\\.ftable|rect\\.hclust|reformulate|regularize\\.values|relevel|relevel\\.default|relevel\\.factor|relevel\\.ordered|reorder|reorder\\.default|reorder\\.dendrogram|replications|reshape|resid|residuals|residuals\\.default|residuals\\.glm|residuals\\.HoltWinters|residuals\\.isoreg|residuals\\.lm|residuals\\.nls|residuals\\.smooth\\.spline|residuals\\.tukeyline|rev\\.dendrogram|rexp|rf|rgamma|rgeom|rhyper|rlnorm|rlogis|rmultinom|rnbinom|rnorm|Roy|rpois|rsignrank|rsmirnov|rstandard|rstandard\\.glm|rstandard\\.lm|rstudent|rstudent\\.glm|rstudent\\.lm|rt|runif|runmed|rweibull|rwilcox|rWishart|safe_pchisq|safe_pf|scatter\\.smooth|screeplot|screeplot\\.default|sd|se\\.aov|se\\.aovlist|se\\.contrast|se\\.contrast\\.aov|se\\.contrast\\.aovlist|selfStart|selfStart\\.default|selfStart\\.formula|setNames|shapiro\\.test|sigma|sigma\\.default|sigma\\.glm|sigma\\.mlm|simpleLoess|simulate|simulate\\.lm|smooth|smooth\\.spline|smoothEnds|sortedXyData|sortedXyData\\.default|spec\\.ar|spec\\.pgram|spec\\.taper|spectrum|sphericity|spl_coef_conv|spline|splinefun|splinefunH|splinefunH0|SSasymp|SSasympOff|SSasympOrig|SSbiexp|SSD|SSD\\.mlm|SSfol|SSfpl|SSgompertz|SSlogis|SSmicmen|SSweibull|start|start\\.default|stat\\.anova|step|stepfun|stl|str\\.dendrogram|str\\.logLik|StructTS|summary\\.aov|summary\\.aovlist|summary\\.ecdf|summary\\.glm|summary\\.infl|summary\\.lm|summary\\.loess|summary\\.manova|summary\\.mlm|summary\\.nls|summary\\.ppr|summary\\.prcomp|summary\\.princomp|summary\\.stepfun|summary\\.stl|summary\\.tukeysmooth|supsmu|symnum|t\\.test|t\\.test\\.default|t\\.test\\.formula|t\\.ts|tail\\.ts|termplot|terms|terms\\.aovlist|terms\\.default|terms\\.formula|terms\\.terms|Thin\\.col|Thin\\.row|time|time\\.default|time\\.ts|toeplitz|toeplitz2|Tr|ts|ts\\.intersect|ts\\.plot|ts\\.union|tsdiag|tsdiag\\.Arima|tsdiag\\.arima0|tsdiag\\.StructTS|tsp|tsSmooth|tsSmooth\\.StructTS|TukeyHSD|TukeyHSD\\.aov|uniroot|update|update\\.default|update\\.formula|update\\.packageStatus|var|var\\.test|var\\.test\\.default|var\\.test\\.formula|variable\\.names|variable\\.names\\.default|variable\\.names\\.lm|varimax|vcov|vcov\\.aov|vcov\\.Arima|vcov\\.glm|vcov\\.lm|vcov\\.mlm|vcov\\.nls|vcov\\.summary\\.glm|vcov\\.summary\\.lm|weighted\\.mean|weighted\\.mean\\.Date|weighted\\.mean\\.default|weighted\\.mean\\.difftime|weighted\\.mean\\.POSIXct|weighted\\.mean\\.POSIXlt|weighted\\.residuals|weights|weights\\.default|weights\\.glm|weights\\.nls|wilcox\\.test|wilcox\\.test\\.default|wilcox\\.test\\.formula|Wilks|window|window\\.default|window\\.ts|write\\.ftable|xtabs)\\s*(\\()", + "beginCaptures": { + "1": { + "name": "support.namespace.r" + }, + "2": { + "name": "punctuation.accessor.colons.r" + }, + "3": { + "name": "support.function.r" + }, + "4": { + "name": "punctuation.definition.arguments.begin.r" + } + }, + "end": "(\\))", + "endCaptures": { + "1": { + "name": "punctuation.definition.arguments.end.r" + } + }, + "patterns": [ + { + "include": "#function-call-arguments" + } + ] + }, + { + "name": "meta.function-call.r", + "contentName": "meta.function-call.arguments.r", + "begin": "\\b(?:(utils)(::))?(adist|alarm|apropos|aregexec|argNames|argsAnywhere|as\\.bibentry|as\\.bibentry\\.bibentry|as\\.bibentry\\.citation|as\\.character\\.person|as\\.character\\.roman|as\\.data\\.frame\\.bibentry|as\\.data\\.frame\\.citation|as\\.data\\.frame\\.person|as\\.environment\\.hashtab|as\\.person|as\\.person\\.default|as\\.personList|as\\.personList\\.default|as\\.personList\\.person|as\\.relistable|as\\.roman|asDateBuilt|askYesNo|aspell|aspell_filter_LaTeX_commands_from_Aspell_tex_filter_info|aspell_filter_LaTeX_worker|aspell_find_dictionaries|aspell_find_program|aspell_inspect_context|aspell_package|aspell_package_C_files|aspell_package_description|aspell_package_pot_files|aspell_package_R_files|aspell_package_Rd_files|aspell_package_vignettes|aspell_query_wiktionary_categories|aspell_R_C_files|aspell_R_manuals|aspell_R_R_files|aspell_R_Rd_files|aspell_R_vignettes|aspell_update_dictionary|aspell_write_personal_dictionary_file|assignInMyNamespace|assignInNamespace|attachedPackageCompletions|available\\.packages|bibentry|blank_out_character_ranges|blank_out_ignores_in_lines|blank_out_regexp_matches|browseEnv|browseURL|browseVignettes|bug\\.report|bug\\.report\\.info|c\\.bibentry|c\\.person|capture\\.output|changedFiles|charClass|check_for_XQuartz|check_screen_device|checkCRAN|checkHT|chooseBioCmirror|chooseCRANmirror|citation|cite|citeNatbib|citEntry|citFooter|citHeader|close\\.socket|close\\.txtProgressBar|clrhash|combn|compareVersion|conformToProto|contrib\\.url|correctFilenameToken|count\\.fields|create\\.post|data|data\\.entry|dataentry|de|de\\.ncols|de\\.restore|de\\.setup|debugcall|debugger|defaultUserAgent|demo|download\\.file|download\\.packages|dump\\.frames|edit|edit\\.data\\.frame|edit\\.default|edit\\.matrix|edit\\.vignette|emacs|example|expr2token|file_test|file\\.edit|fileCompletionPreferred|fileCompletions|fileSnapshot|filter_packages_by_depends_predicates|find|find_files_in_directories|findCRANmirror|findExactMatches|findFuzzyMatches|findGeneric|findLineNum|findMatches|fix|fixInNamespace|flush\\.console|fnLineNum|format\\.aspell|format\\.aspell_inspect_context|format\\.bibentry|format\\.citation|format\\.hashtab|format\\.MethodsFunction|format\\.news_db|format\\.object_size|format\\.person|format\\.roman|formatOL|formatUL|functionArgs|fuzzyApropos|get_parse_data_for_message_strings|getAnywhere|getCRANmirrors|getDependencies|getFromNamespace|gethash|getIsFirstArg|getKnownS3generics|getParseData|getParseText|getRcode|getRcode\\.vignette|getS3method|getSrcByte|getSrcDirectory|getSrcfile|getSrcFilename|getSrcLocation|getSrcref|getTxtProgressBar|glob2rx|globalVariables|hashtab|hasName|head|head\\.array|head\\.default|head\\.ftable|head\\.function|head\\.matrix|help|help\\.request|help\\.search|help\\.start|helpCompletions|history|hsearch_db|hsearch_db_concepts|hsearch_db_keywords|index\\.search|inFunction|install\\.packages|installed\\.packages|is\\.hashtab|is\\.relistable|isBasePkg|isInsideQuotes|isS3method|isS3stdGeneric|keywordCompletions|length\\.hashtab|limitedLabels|loadedPackageCompletions|loadhistory|localeToCharset|ls\\.str|lsf\\.str|macDynLoads|maintainer|make_sysdata_rda|make\\.packages\\.html|make\\.socket|makeRegexpSafe|makeRweaveLatexCodeRunner|makeUserAgent|maphash|matchAvailableTopics|memory\\.limit|memory\\.size|menu|merge_demo_index|merge_vignette_index|methods|mirror2html|modifyList|new\\.packages|news|normalCompletions|nsl|numhash|object\\.size|offline_help_helper|old\\.packages|Ops\\.roman|package\\.skeleton|packageDate|packageDescription|packageName|packageStatus|packageVersion|page|person|personList|pico|print\\.aspell|print\\.aspell_inspect_context|print\\.bibentry|print\\.Bibtex|print\\.browseVignettes|print\\.changedFiles|print\\.citation|print\\.fileSnapshot|print\\.findLineNumResult|print\\.getAnywhere|print\\.hashtab|print\\.help_files_with_topic|print\\.hsearch|print\\.hsearch_db|print\\.Latex|print\\.ls_str|print\\.MethodsFunction|print\\.news_db|print\\.object_size|print\\.packageDescription|print\\.packageIQR|print\\.packageStatus|print\\.person|print\\.roman|print\\.sessionInfo|print\\.socket|print\\.summary\\.packageStatus|print\\.vignette|printhsearchInternal|process\\.events|prompt|prompt\\.data\\.frame|prompt\\.default|promptData|promptImport|promptPackage|rc\\.getOption|rc\\.options|rc\\.settings|rc\\.status|read\\.csv|read\\.csv2|read\\.delim|read\\.delim2|read\\.DIF|read\\.fortran|read\\.fwf|read\\.socket|read\\.table|readCitationFile|recover|registerNames|regquote|relist|relist\\.default|relist\\.factor|relist\\.list|relist\\.matrix|remhash|remove\\.packages|removeSource|rep\\.bibentry|rep\\.person|rep\\.roman|resolvePkgType|Rprof|Rprof_memory_summary|Rprofmem|RShowDoc|RSiteSearch|rtags|rtags\\.file|Rtangle|RtangleFinish|RtangleRuncode|RtangleSetup|RtangleWritedoc|RweaveChunkPrefix|RweaveEvalWithOpt|RweaveLatex|RweaveLatexFinish|RweaveLatexOptions|RweaveLatexRuncode|RweaveLatexSetup|RweaveLatexWritedoc|RweaveTryStop|savehistory|select\\.list|sessionInfo|setBreakpoint|sethash|setIsFirstArg|setRepositories|setTxtProgressBar|shorten\\.to\\.string|simplifyRepos|sort\\.bibentry|specialCompletions|specialFunctionArgs|specialOpCompletionsHelper|specialOpLocs|stack|stack\\.data\\.frame|stack\\.default|Stangle|str|str\\.data\\.frame|str\\.Date|str\\.default|str\\.hashtab|str\\.POSIXt|str2logical|strcapture|strextract|strOptions|strslice|subset\\.news_db|substr_with_tabs|summary\\.aspell|summary\\.packageStatus|Summary\\.roman|summaryRprof|suppressForeignCheck|Sweave|SweaveGetSyntax|SweaveHooks|SweaveParseOptions|SweaveReadFile|SweaveSyntConv|tail|tail\\.array|tail\\.default|tail\\.ftable|tail\\.function|tail\\.matrix|tar|timestamp|toBibtex|toBibtex\\.bibentry|toBibtex\\.person|toLatex|toLatex\\.sessionInfo|toLatexPDlist|topicName|transform\\.bibentry|txtProgressBar|type\\.convert|type\\.convert\\.data\\.frame|type\\.convert\\.default|type\\.convert\\.list|typhash|undebugcall|unique\\.bibentry|unique\\.person|unlist\\.relistable|unstack|unstack\\.data\\.frame|unstack\\.default|untar|untar2|unzip|update\\.packages|update\\.packageStatus|upgrade|upgrade\\.packageStatus|url\\.show|URLdecode|URLencode|vi|View|vignette|warnErrList|write\\.csv|write\\.csv2|write\\.ctags|write\\.etags|write\\.socket|write\\.table|wsbrowser|xedit|xemacs|zip)\\s*(\\()", + "beginCaptures": { + "1": { + "name": "support.namespace.r" + }, + "2": { + "name": "punctuation.accessor.colons.r" + }, + "3": { + "name": "support.function.r" + }, + "4": { + "name": "punctuation.definition.arguments.begin.r" + } + }, + "end": "(\\))", + "endCaptures": { + "1": { + "name": "punctuation.definition.arguments.end.r" + } + }, + "patterns": [ + { + "include": "#function-call-arguments" + } + ] + } + ] + }, + "comments": { + "patterns": [ + { + "name": "comment.line.pragma-mark.r", + "match": "^(#pragma[ \\t]+mark)[ \\t](.*)", + "captures": { + "1": { + "name": "comment.line.pragma.r" + }, + "2": { + "name": "entity.name.pragma.name.r" + } + } + }, + { + "begin": "(^[ \\t]+)?(?=#)", + "beginCaptures": { + "1": { + "name": "punctuation.whitespace.comment.leading.r" + } + }, + "end": "(?!\\G)", + "patterns": [ + { + "name": "comment.line.number-sign.r", + "begin": "#", + "beginCaptures": { + "0": { + "name": "punctuation.definition.comment.r" + } + }, + "end": "\\n" + } + ] + } + ] + }, + "constants": { + "patterns": [ + { + "name": "support.constant.misc.r", + "match": "\\b(pi|letters|LETTERS|month\\.abb|month\\.name)\\b" + }, + { + "name": "constant.language.r", + "match": "\\b(TRUE|FALSE|NULL|NA|NA_integer_|NA_real_|NA_complex_|NA_character_|Inf|NaN)\\b" + }, + { + "name": "constant.numeric.imaginary.hexadecimal.r", + "match": "\\b0(x|X)[0-9a-fA-F]+i\\b" + }, + { + "name": "constant.numeric.imaginary.decimal.r", + "match": "\\b[0-9]+\\.?[0-9]*(?:(e|E)(\\+|-)?[0-9]+)?i\\b" + }, + { + "name": "constant.numeric.imaginary.decimal.r", + "match": "\\.[0-9]+(?:(e|E)(\\+|-)?[0-9]+)?i\\b" + }, + { + "name": "constant.numeric.integer.hexadecimal.r", + "match": "\\b0(x|X)[0-9a-fA-F]+L\\b" + }, + { + "name": "constant.numeric.integer.decimal.r", + "match": "\\b(?:[0-9]+\\.?[0-9]*)(?:(e|E)(\\+|-)?[0-9]+)?L\\b" + }, + { + "name": "constant.numeric.float.hexadecimal.r", + "match": "\\b0(x|X)[0-9a-fA-F]+\\b" + }, + { + "name": "constant.numeric.float.decimal.r", + "match": "\\b[0-9]+\\.?[0-9]*(?:(e|E)(\\+|-)?[0-9]+)?\\b" + }, + { + "name": "constant.numeric.float.decimal.r", + "match": "\\.[0-9]+(?:(e|E)(\\+|-)?[0-9]+)?\\b" + } + ] + }, + "function-call-arguments": { + "patterns": [ + { + "name": "variable.parameter.function-call.r", + "match": "(?:[a-zA-Z._][\\w.]*|`[^`]+`)(?=\\s*=[^=])" + }, + { + "begin": "(?==)", + "end": "(?=[,)])", + "patterns": [ + { + "include": "source.r" + } + ] + }, + { + "name": "punctuation.separator.parameters.r", + "match": "," + }, + { + "include": "source.r" + } + ] + }, + "function-calls": { + "name": "meta.function-call.r", + "contentName": "meta.function-call.arguments.r", + "begin": "(?:[a-zA-Z._][\\w.]*|`[^`]+`)\\s*(\\()", + "beginCaptures": { + "1": { + "name": "punctuation.definition.arguments.begin.r" + } + }, + "end": "(\\))", + "endCaptures": { + "1": { + "name": "punctuation.definition.arguments.end.r" + } + }, + "patterns": [ + { + "include": "#function-call-arguments" + } + ] + }, "function-declarations": { "patterns": [ { + "name": "meta.function.r", + "contentName": "meta.function.parameters.r", "begin": "((?:[a-zA-Z._][\\w.]*|`[^`]+`))\\s*(>)" }, { - "match": ",", - "name": "punctuation.separator.parameters.r" + "name": "keyword.operator.other.r", + "match": "%(between|chin|do|dopar|in|like|\\+replace|\\+|:|T>|<>|>|\\$)%" }, { - "include": "source.r" + "name": "keyword.other.r", + "match": "\\.\\.\\." + }, + { + "name": "punctuation.accessor.colons.r", + "match": ":::?" + }, + { + "name": "keyword.operator.arithmetic.r", + "match": "(%%|\\*\\*)" + }, + { + "name": "keyword.operator.assignment.r", + "match": "(<-|->)" + }, + { + "name": "keyword.operator.pipe.r", + "match": "\\|>" + }, + { + "name": "keyword.operator.comparison.r", + "match": "(==|!=|<>|<=?|>=?)" + }, + { + "name": "keyword.operator.logical.r", + "match": "(&&?|\\|\\|?)" + }, + { + "name": "keyword.operator.other.r", + "match": ":=" + }, + { + "name": "keyword.operator.arithmetic.r", + "match": "[-+*/^]" + }, + { + "name": "keyword.operator.assignment.r", + "match": "=" + }, + { + "name": "keyword.operator.logical.r", + "match": "!" + }, + { + "name": "keyword.other.r", + "match": "[:~@]" + }, + { + "name": "punctuation.terminator.semicolon.r", + "match": ";" } ] }, "roxygen": { "patterns": [ { + "name": "comment.line.roxygen.r", "begin": "^\\s*(#')\\s*", "beginCaptures": { "1": { @@ -653,9 +687,9 @@ } }, "end": "$\\n?", - "name": "comment.line.roxygen.r", "patterns": [ { + "match": "(@param)\\s*((?:[a-zA-Z._][\\w.]*|`[^`]+`))", "captures": { "1": { "name": "keyword.other.r" @@ -663,160 +697,175 @@ "2": { "name": "variable.parameter.r" } - }, - "match": "(@param)\\s*((?:[a-zA-Z._][\\w.]*|`[^`]+`))" + } }, { - "match": "@[a-zA-Z0-9]+", - "name": "keyword.other.r" + "name": "keyword.other.r", + "match": "@[a-zA-Z0-9]+" } ] } ] }, - "builtin-functions": { + "storage-type": { "patterns": [ { - "begin": "\\b(abbreviate|abs|acos|acosh|activeBindingFunction|addNA|addTaskCallback|agrep|agrepl|alist|all|all\\.equal|all\\.equal\\.character|all\\.equal\\.default|all\\.equal\\.environment|all\\.equal\\.envRefClass|all\\.equal\\.factor|all\\.equal\\.formula|all\\.equal\\.function|all\\.equal\\.language|all\\.equal\\.list|all\\.equal\\.numeric|all\\.equal\\.POSIXt|all\\.equal\\.raw|all\\.names|allowInterrupts|all\\.vars|any|anyDuplicated|anyDuplicated\\.array|anyDuplicated\\.data\\.frame|anyDuplicated\\.default|anyDuplicated\\.matrix|anyNA|anyNA\\.data\\.frame|anyNA\\.numeric_version|anyNA\\.POSIXlt|aperm|aperm\\.default|aperm\\.table|append|apply|Arg|args|array|arrayInd|as\\.array|as\\.array\\.default|as\\.call|as\\.character|as\\.character\\.condition|as\\.character\\.Date|as\\.character\\.default|as\\.character\\.error|as\\.character\\.factor|as\\.character\\.hexmode|as\\.character\\.numeric_version|as\\.character\\.octmode|as\\.character\\.POSIXt|as\\.character\\.srcref|as\\.complex|as\\.data\\.frame|as\\.data\\.frame\\.array|as\\.data\\.frame\\.AsIs|as\\.data\\.frame\\.character|as\\.data\\.frame\\.complex|as\\.data\\.frame\\.data\\.frame|as\\.data\\.frame\\.Date|as\\.data\\.frame\\.default|as\\.data\\.frame\\.difftime|as\\.data\\.frame\\.factor|as\\.data\\.frame\\.integer|as\\.data\\.frame\\.list|as\\.data\\.frame\\.logical|as\\.data\\.frame\\.matrix|as\\.data\\.frame\\.model\\.matrix|as\\.data\\.frame\\.noquote|as\\.data\\.frame\\.numeric|as\\.data\\.frame\\.numeric_version|as\\.data\\.frame\\.ordered|as\\.data\\.frame\\.POSIXct|as\\.data\\.frame\\.POSIXlt|as\\.data\\.frame\\.raw|as\\.data\\.frame\\.table|as\\.data\\.frame\\.ts|as\\.data\\.frame\\.vector|as\\.Date|as\\.Date\\.character|as\\.Date\\.default|as\\.Date\\.factor|as\\.Date\\.numeric|as\\.Date\\.POSIXct|as\\.Date\\.POSIXlt|as\\.difftime|as\\.double|as\\.double\\.difftime|as\\.double\\.POSIXlt|as\\.environment|as\\.expression|as\\.expression\\.default|as\\.factor|as\\.function|as\\.function\\.default|as\\.hexmode|asin|asinh|as\\.integer|as\\.list|as\\.list\\.data\\.frame|as\\.list\\.Date|as\\.list\\.default|as\\.list\\.difftime|as\\.list\\.environment|as\\.list\\.factor|as\\.list\\.function|as\\.list\\.numeric_version|as\\.list\\.POSIXct|as\\.list\\.POSIXlt|as\\.logical|as\\.logical\\.factor|as\\.matrix|as\\.matrix\\.data\\.frame|as\\.matrix\\.default|as\\.matrix\\.noquote|as\\.matrix\\.POSIXlt|as\\.name|asNamespace|as\\.null|as\\.null\\.default|as\\.numeric|as\\.numeric_version|as\\.octmode|as\\.ordered|as\\.package_version|as\\.pairlist|asplit|as\\.POSIXct|as\\.POSIXct\\.Date|as\\.POSIXct\\.default|as\\.POSIXct\\.numeric|as\\.POSIXct\\.POSIXlt|as\\.POSIXlt|as\\.POSIXlt\\.character|as\\.POSIXlt\\.Date|as\\.POSIXlt\\.default|as\\.POSIXlt\\.factor|as\\.POSIXlt\\.numeric|as\\.POSIXlt\\.POSIXct|as\\.qr|as\\.raw|asS3|asS4|assign|as\\.single|as\\.single\\.default|as\\.symbol|as\\.table|as\\.table\\.default|as\\.vector|as\\.vector\\.factor|atan|atan2|atanh|attach|attachNamespace|attr|attr\\.all\\.equal|attributes|autoload|autoloader|backsolve|baseenv|basename|besselI|besselJ|besselK|besselY|beta|bindingIsActive|bindingIsLocked|bindtextdomain|bitwAnd|bitwNot|bitwOr|bitwShiftL|bitwShiftR|bitwXor|body|bquote|break|browser|browserCondition|browserSetDebug|browserText|builtins|by|by\\.data\\.frame|by\\.default|bzfile|c|call|callCC|capabilities|casefold|cat|cbind|cbind\\.data\\.frame|c\\.Date|c\\.difftime|ceiling|c\\.factor|character|char\\.expand|charmatch|charToRaw|chartr|check_tzones|chkDots|chol|chol2inv|chol\\.default|choose|class|clearPushBack|close|closeAllConnections|close\\.connection|close\\.srcfile|close\\.srcfilealias|c\\.noquote|c\\.numeric_version|col|colMeans|colnames|colSums|commandArgs|comment|complex|computeRestarts|conditionCall|conditionCall\\.condition|conditionMessage|conditionMessage\\.condition|conflictRules|conflicts|Conj|contributors|cos|cosh|cospi|c\\.POSIXct|c\\.POSIXlt|crossprod|Cstack_info|cummax|cummin|cumprod|cumsum|curlGetHeaders|cut|cut\\.Date|cut\\.default|cut\\.POSIXt|c\\.warnings|data\\.class|data\\.frame|data\\.matrix|date|debug|debuggingState|debugonce|default\\.stringsAsFactors|delayedAssign|deparse|deparse1|det|detach|determinant|determinant\\.matrix|dget|diag|diff|diff\\.Date|diff\\.default|diff\\.difftime|diff\\.POSIXt|difftime|digamma|dim|dim\\.data\\.frame|dimnames|dimnames\\.data\\.frame|dir|dir\\.create|dir\\.exists|dirname|do\\.call|dontCheck|double|dput|dQuote|drop|droplevels|droplevels\\.data\\.frame|droplevels\\.factor|dump|duplicated|duplicated\\.array|duplicated\\.data\\.frame|duplicated\\.default|duplicated\\.matrix|duplicated\\.numeric_version|duplicated\\.POSIXlt|duplicated\\.warnings|dynGet|dyn\\.load|dyn\\.unload|eapply|eigen|emptyenv|enc2native|enc2utf8|encodeString|Encoding|endsWith|enquote|environment|environmentIsLocked|environmentName|env\\.profile|errorCondition|eval|eval\\.parent|evalq|exists|exp|expand\\.grid|expm1|expression|extSoftVersion|factor|factorial|fifo|file|file\\.access|file\\.append|file\\.choose|file\\.copy|file\\.create|file\\.exists|file\\.info|file\\.link|file\\.mode|file\\.mtime|file\\.path|file\\.remove|file\\.rename|file\\.show|file\\.size|file\\.symlink|Filter|Find|findInterval|find\\.package|findPackageEnv|findRestart|floor|flush|flush\\.connection|for|force|forceAndCall|formals|format|format\\.AsIs|formatC|format\\.data\\.frame|format\\.Date|format\\.default|format\\.difftime|formatDL|format\\.factor|format\\.hexmode|format\\.info|format\\.libraryIQR|format\\.numeric_version|format\\.octmode|format\\.packageInfo|format\\.POSIXct|format\\.POSIXlt|format\\.pval|format\\.summaryDefault|forwardsolve|function|gamma|gc|gcinfo|gc\\.time|gctorture|gctorture2|get|get0|getAllConnections|getCallingDLL|getCallingDLLe|getConnection|getDLLRegisteredRoutines|getDLLRegisteredRoutines\\.character|getDLLRegisteredRoutines\\.DLLInfo|getElement|geterrmessage|getExportedValue|getHook|getLoadedDLLs|getNamespace|getNamespaceExports|getNamespaceImports|getNamespaceInfo|getNamespaceName|getNamespaceUsers|getNamespaceVersion|getNativeSymbolInfo|getOption|getRversion|getSrcLines|getTaskCallbackNames|gettext|gettextf|getwd|gl|globalCallingHandlers|globalenv|gregexec|gregexpr|grep|grepl|grepRaw|grouping|gsub|gzcon|gzfile|I|iconv|iconvlist|icuGetCollate|icuSetCollate|identical|identity|if|ifelse|Im|importIntoEnv|infoRDS|inherits|integer|interaction|interactive|intersect|intToBits|intToUtf8|inverse\\.rle|invisible|invokeRestart|invokeRestartInteractively|isa|is\\.array|is\\.atomic|isatty|isBaseNamespace|is\\.call|is\\.character|is\\.complex|is\\.data\\.frame|isdebugged|is\\.double|is\\.element|is\\.environment|is\\.expression|is\\.factor|isFALSE|is\\.finite|is\\.function|isIncomplete|is\\.infinite|is\\.integer|is\\.language|is\\.list|is\\.loaded|is\\.logical|is\\.matrix|is\\.na|is\\.na\\.data\\.frame|is\\.name|isNamespace|isNamespaceLoaded|is\\.nan|is\\.na\\.numeric_version|is\\.na\\.POSIXlt|is\\.null|is\\.numeric|is\\.numeric\\.Date|is\\.numeric\\.difftime|is\\.numeric\\.POSIXt|is\\.numeric_version|is\\.object|ISOdate|ISOdatetime|isOpen|is\\.ordered|is\\.package_version|is\\.pairlist|is\\.primitive|is\\.qr|is\\.R|is\\.raw|is\\.recursive|isRestart|isS4|isSeekable|is\\.single|is\\.symbol|isSymmetric|isSymmetric\\.matrix|is\\.table|isTRUE|is\\.unsorted|is\\.vector|jitter|julian|julian\\.Date|julian\\.POSIXt|kappa|kappa\\.default|kappa\\.lm|kappa\\.qr|kronecker|l10n_info|labels|labels\\.default|La_library|lapply|La\\.svd|La_version|lazyLoad|lazyLoadDBexec|lazyLoadDBfetch|lbeta|lchoose|length|length\\.POSIXlt|lengths|levels|levels\\.default|lfactorial|lgamma|libcurlVersion|library|library\\.dynam|library\\.dynam\\.unload|licence|license|list|list2DF|list2env|list\\.dirs|list\\.files|load|loadedNamespaces|loadingNamespaceInfo|loadNamespace|local|lockBinding|lockEnvironment|log|log10|log1p|log2|logb|logical|lower\\.tri|ls|makeActiveBinding|make\\.names|make\\.unique|Map|mapply|marginSums|margin\\.table|match|match\\.arg|match\\.call|match\\.fun|Math\\.data\\.frame|Math\\.Date|Math\\.difftime|Math\\.factor|Math\\.POSIXt|mat\\.or\\.vec|matrix|max|max\\.col|mean|mean\\.Date|mean\\.default|mean\\.difftime|mean\\.POSIXct|mean\\.POSIXlt|memCompress|memDecompress|mem\\.maxNSize|mem\\.maxVSize|memory\\.profile|merge|merge\\.data\\.frame|merge\\.default|message|mget|min|missing|Mod|mode|months|months\\.Date|months\\.POSIXt|names|namespaceExport|namespaceImport|namespaceImportClasses|namespaceImportFrom|namespaceImportMethods|names\\.POSIXlt|nargs|nchar|ncol|NCOL|Negate|new\\.env|next|NextMethod|ngettext|nlevels|noquote|norm|normalizePath|nrow|NROW|nullfile|numeric|numeric_version|numToBits|numToInts|nzchar|objects|oldClass|OlsonNames|on\\.exit|open|open\\.connection|open\\.srcfile|open\\.srcfilealias|open\\.srcfilecopy|Ops\\.data\\.frame|Ops\\.Date|Ops\\.difftime|Ops\\.factor|Ops\\.numeric_version|Ops\\.ordered|Ops\\.POSIXt|options|order|ordered|outer|packageEvent|packageHasNamespace|packageNotFoundError|packageStartupMessage|package_version|packBits|pairlist|parent\\.env|parent\\.frame|parse|parseNamespaceFile|paste|paste0|path\\.expand|path\\.package|pcre_config|pi|pipe|plot|pmatch|pmax|pmax\\.int|pmin|pmin\\.int|polyroot|Position|pos\\.to\\.env|pretty|pretty\\.default|prettyNum|print|print\\.AsIs|print\\.by|print\\.condition|print\\.connection|print\\.data\\.frame|print\\.Date|print\\.default|print\\.difftime|print\\.Dlist|print\\.DLLInfo|print\\.DLLInfoList|print\\.DLLRegisteredRoutines|print\\.eigen|print\\.factor|print\\.function|print\\.hexmode|print\\.libraryIQR|print\\.listof|print\\.NativeRoutineList|print\\.noquote|print\\.numeric_version|print\\.octmode|print\\.packageInfo|print\\.POSIXct|print\\.POSIXlt|print\\.proc_time|print\\.restart|print\\.rle|print\\.simple\\.list|print\\.srcfile|print\\.srcref|print\\.summaryDefault|print\\.summary\\.table|print\\.summary\\.warnings|print\\.table|print\\.warnings|prmatrix|proc\\.time|prod|proportions|prop\\.table|provideDimnames|psigamma|pushBack|pushBackLength|q|qr|qr\\.coef|qr\\.default|qr\\.fitted|qr\\.Q|qr\\.qty|qr\\.qy|qr\\.R|qr\\.resid|qr\\.solve|qr\\.X|quarters|quarters\\.Date|quarters\\.POSIXt|quit|quote|range|range\\.default|rank|rapply|raw|rawConnection|rawConnectionValue|rawShift|rawToBits|rawToChar|rbind|rbind\\.data\\.frame|rcond|Re|readBin|readChar|read\\.dcf|readline|readLines|readRDS|readRenviron|Recall|Reduce|regexec|regexpr|reg\\.finalizer|registerS3method|registerS3methods|regmatches|remove|removeTaskCallback|rep|rep\\.Date|rep\\.difftime|repeat|rep\\.factor|rep\\.int|replace|rep_len|replicate|rep\\.numeric_version|rep\\.POSIXct|rep\\.POSIXlt|require|requireNamespace|restartDescription|restartFormals|retracemem|return|returnValue|rev|rev\\.default|R\\.home|rle|rm|RNGkind|RNGversion|round|round\\.Date|round\\.POSIXt|row|rowMeans|rownames|row\\.names|row\\.names\\.data\\.frame|row\\.names\\.default|rowsum|rowsum\\.data\\.frame|rowsum\\.default|rowSums|R_system_version|R\\.version|R\\.Version|R\\.version\\.string|sample|sample\\.int|sapply|save|save\\.image|saveRDS|scale|scale\\.default|scan|search|searchpaths|seek|seek\\.connection|seq|seq_along|seq\\.Date|seq\\.default|seq\\.int|seq_len|seq\\.POSIXt|sequence|sequence\\.default|serialize|serverSocket|setdiff|setequal|setHook|setNamespaceInfo|set\\.seed|setSessionTimeLimit|setTimeLimit|setwd|showConnections|shQuote|sign|signalCondition|signif|simpleCondition|simpleError|simpleMessage|simpleWarning|simplify2array|sin|single|sinh|sink|sink\\.number|sinpi|slice\\.index|socketAccept|socketConnection|socketSelect|socketTimeout|solve|solve\\.default|solve\\.qr|sort|sort\\.default|sort\\.int|sort\\.list|sort\\.POSIXlt|source|split|split\\.data\\.frame|split\\.Date|split\\.default|split\\.POSIXct|sprintf|sqrt|sQuote|srcfile|srcfilealias|srcfilecopy|srcref|standardGeneric|startsWith|stderr|stdin|stdout|stop|stopifnot|storage\\.mode|str2expression|str2lang|strftime|strptime|strrep|strsplit|strtoi|strtrim|structure|strwrap|sub|subset|subset\\.data\\.frame|subset\\.default|subset\\.matrix|substitute|substr|substring|sum|summary|summary\\.connection|summary\\.data\\.frame|Summary\\.data\\.frame|summary\\.Date|Summary\\.Date|summary\\.default|Summary\\.difftime|summary\\.factor|Summary\\.factor|summary\\.matrix|Summary\\.numeric_version|Summary\\.ordered|summary\\.POSIXct|Summary\\.POSIXct|summary\\.POSIXlt|Summary\\.POSIXlt|summary\\.proc_time|summary\\.srcfile|summary\\.srcref|summary\\.table|summary\\.warnings|suppressMessages|suppressPackageStartupMessages|suppressWarnings|suspendInterrupts|svd|sweep|switch|sys\\.call|sys\\.calls|Sys\\.chmod|Sys\\.Date|sys\\.frame|sys\\.frames|sys\\.function|Sys\\.getenv|Sys\\.getlocale|Sys\\.getpid|Sys\\.glob|Sys\\.info|sys\\.load\\.image|Sys\\.localeconv|sys\\.nframe|sys\\.on\\.exit|sys\\.parent|sys\\.parents|Sys\\.readlink|sys\\.save\\.image|Sys\\.setenv|Sys\\.setFileTime|Sys\\.setlocale|Sys\\.sleep|sys\\.source|sys\\.status|system|system2|system\\.file|system\\.time|Sys\\.time|Sys\\.timezone|Sys\\.umask|Sys\\.unsetenv|Sys\\.which|t|table|tabulate|tan|tanh|tanpi|tapply|taskCallbackManager|tcrossprod|t\\.data\\.frame|t\\.default|tempdir|tempfile|textConnection|textConnectionValue|tolower|topenv|toString|toString\\.default|toupper|trace|traceback|tracemem|tracingState|transform|transform\\.data\\.frame|transform\\.default|trigamma|trimws|trunc|truncate|truncate\\.connection|trunc\\.Date|trunc\\.POSIXt|try|tryCatch|tryInvokeRestart|typeof|unclass|undebug|union|unique|unique\\.array|unique\\.data\\.frame|unique\\.default|unique\\.matrix|unique\\.numeric_version|unique\\.POSIXlt|unique\\.warnings|units|units\\.difftime|unix\\.time|unlink|unlist|unloadNamespace|unlockBinding|unname|unserialize|unsplit|untrace|untracemem|unz|upper\\.tri|url|UseMethod|utf8ToInt|validEnc|validUTF8|vapply|vector|Vectorize|version|warning|warningCondition|warnings|weekdays|weekdays\\.Date|weekdays\\.POSIXt|which|which\\.max|which\\.min|while|with|withAutoprint|withCallingHandlers|with\\.default|within|within\\.data\\.frame|within\\.list|withRestarts|withVisible|write|writeBin|writeChar|write\\.dcf|writeLines|xor|xpdrows\\.data\\.frame|xtfrm|xtfrm\\.AsIs|xtfrm\\.data\\.frame|xtfrm\\.Date|xtfrm\\.default|xtfrm\\.difftime|xtfrm\\.factor|xtfrm\\.numeric_version|xtfrm\\.POSIXct|xtfrm\\.POSIXlt|xzfile|zapsmall)\\s*(\\()", + "name": "meta.function-call.r", + "contentName": "meta.function-call.arguments.r", + "begin": "\\b(character|complex|double|expression|integer|list|logical|numeric|single|raw|pairlist)\\b\\s*(\\()", "beginCaptures": { "1": { - "name": "support.function.r" + "name": "storage.type.r" }, "2": { "name": "punctuation.definition.arguments.begin.r" } }, - "contentName": "meta.function-call.arguments.r", "end": "(\\))", "endCaptures": { "1": { "name": "punctuation.definition.arguments.end.r" } }, - "name": "meta.function-call.r", "patterns": [ { "include": "#function-call-arguments" } ] + } + ] + }, + "strings": { + "patterns": [ + { + "name": "string.quoted.double.raw.r", + "begin": "[rR]\"(-*)\\[", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.raw.begin.r" + } + }, + "end": "\\]\\1\"", + "endCaptures": { + "0": { + "name": "punctuation.definition.string.raw.end.r" + } + } }, { - "begin": "\\b(abline|arrows|assocplot|axis|Axis|axis\\.Date|axis\\.POSIXct|axTicks|barplot|barplot\\.default|box|boxplot|boxplot\\.default|boxplot\\.matrix|bxp|cdplot|clip|close\\.screen|co\\.intervals|contour|contour\\.default|coplot|curve|dotchart|erase\\.screen|filled\\.contour|fourfoldplot|frame|grconvertX|grconvertY|grid|hist|hist\\.default|identify|image|image\\.default|layout|layout\\.show|lcm|legend|lines|lines\\.default|locator|matlines|matplot|matpoints|mosaicplot|mtext|pairs|pairs\\.default|panel\\.smooth|par|persp|pie|plot|plot\\.default|plot\\.design|plot\\.function|plot\\.new|plot\\.window|plot\\.xy|points|points\\.default|polygon|polypath|rasterImage|rect|rug|screen|segments|smoothScatter|spineplot|split\\.screen|stars|stem|strheight|stripchart|strwidth|sunflowerplot|symbols|text|text\\.default|title|xinch|xspline|xyinch|yinch)\\s*(\\()", + "name": "string.quoted.single.raw.r", + "begin": "[rR]'(-*)\\[", "beginCaptures": { - "1": { - "name": "support.function.r" - }, - "2": { - "name": "punctuation.definition.arguments.begin.r" + "0": { + "name": "punctuation.definition.string.raw.begin.r" } }, - "contentName": "meta.function-call.arguments.r", - "end": "(\\))", + "end": "\\]\\1'", "endCaptures": { - "1": { - "name": "punctuation.definition.arguments.end.r" + "0": { + "name": "punctuation.definition.string.raw.end.r" + } + } + }, + { + "name": "string.quoted.double.raw.r", + "begin": "[rR]\"(-*)\\{", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.raw.begin.r" + } + }, + "end": "\\}\\1\"", + "endCaptures": { + "0": { + "name": "punctuation.definition.string.raw.end.r" + } + } + }, + { + "name": "string.quoted.single.raw.r", + "begin": "[rR]'(-*)\\{", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.raw.begin.r" + } + }, + "end": "\\}\\1'", + "endCaptures": { + "0": { + "name": "punctuation.definition.string.raw.end.r" + } + } + }, + { + "name": "string.quoted.double.raw.r", + "begin": "[rR]\"(-*)\\(", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.raw.begin.r" + } + }, + "end": "\\)\\1\"", + "endCaptures": { + "0": { + "name": "punctuation.definition.string.raw.end.r" + } + } + }, + { + "name": "string.quoted.single.raw.r", + "begin": "[rR]'(-*)\\(", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.raw.begin.r" + } + }, + "end": "\\)\\1'", + "endCaptures": { + "0": { + "name": "punctuation.definition.string.raw.end.r" + } + } + }, + { + "name": "string.quoted.double.r", + "begin": "\"", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.r" + } + }, + "end": "\"", + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.r" } }, - "name": "meta.function-call.r", "patterns": [ { - "include": "#function-call-arguments" + "name": "constant.character.escape.r", + "match": "\\\\." } ] }, { - "begin": "\\b(adjustcolor|as\\.graphicsAnnot|as\\.raster|axisTicks|bitmap|blues9|bmp|boxplot\\.stats|cairo_pdf|cairo_ps|cairoSymbolFont|check\\.options|chull|CIDFont|cm|cm\\.colors|col2rgb|colorConverter|colorRamp|colorRampPalette|colors|colorspaces|colours|contourLines|convertColor|densCols|dev2bitmap|devAskNewPage|dev\\.capabilities|dev\\.capture|dev\\.control|dev\\.copy|dev\\.copy2eps|dev\\.copy2pdf|dev\\.cur|dev\\.flush|dev\\.hold|deviceIsInteractive|dev\\.interactive|dev\\.list|dev\\.new|dev\\.next|dev\\.off|dev\\.prev|dev\\.print|dev\\.set|dev\\.size|embedFonts|extendrange|getGraphicsEvent|getGraphicsEventEnv|graphics\\.off|gray|gray\\.colors|grey|grey\\.colors|grSoftVersion|hcl|hcl\\.colors|hcl\\.pals|heat\\.colors|Hershey|hsv|is\\.raster|jpeg|make\\.rgb|n2mfrow|nclass\\.FD|nclass\\.scott|nclass\\.Sturges|palette|palette\\.colors|palette\\.pals|pdf|pdfFonts|pdf\\.options|pictex|png|postscript|postscriptFonts|ps\\.options|quartz|quartzFont|quartzFonts|quartz\\.options|quartz\\.save|rainbow|recordGraphics|recordPlot|replayPlot|rgb|rgb2hsv|savePlot|setEPS|setGraphicsEventEnv|setGraphicsEventHandlers|setPS|svg|terrain\\.colors|tiff|topo\\.colors|trans3d|Type1Font|x11|X11|X11Font|X11Fonts|X11\\.options|xfig|xy\\.coords|xyTable|xyz\\.coords)\\s*(\\()", + "name": "string.quoted.single.r", + "begin": "'", "beginCaptures": { - "1": { - "name": "support.function.r" - }, - "2": { - "name": "punctuation.definition.arguments.begin.r" + "0": { + "name": "punctuation.definition.string.begin.r" } }, - "contentName": "meta.function-call.arguments.r", - "end": "(\\))", + "end": "'", "endCaptures": { - "1": { - "name": "punctuation.definition.arguments.end.r" + "0": { + "name": "punctuation.definition.string.end.r" } }, - "name": "meta.function-call.r", "patterns": [ { - "include": "#function-call-arguments" - } - ] - }, - { - "begin": "\\b(addNextMethod|allNames|Arith|as|asMethodDefinition|assignClassDef|assignMethodsMetaData|balanceMethodsList|cacheGenericsMetaData|cacheMetaData|cacheMethod|callGeneric|callNextMethod|canCoerce|cbind2|checkAtAssignment|checkSlotAssignment|classesToAM|classLabel|classMetaName|className|coerce|Compare|completeClassDefinition|completeExtends|completeSubclasses|Complex|conformMethod|defaultDumpName|defaultPrototype|doPrimitiveMethod|dumpMethod|dumpMethods|el|elNamed|empty\\.dump|emptyMethodsList|evalOnLoad|evalqOnLoad|evalSource|existsFunction|existsMethod|extends|externalRefMethod|finalDefaultMethod|findClass|findFunction|findMethod|findMethods|findMethodSignatures|findUnique|fixPre1\\.8|formalArgs|functionBody|generic\\.skeleton|getAllSuperClasses|getClass|getClassDef|getClasses|getDataPart|getFunction|getGeneric|getGenerics|getGroup|getGroupMembers|getLoadActions|getMethod|getMethods|getMethodsForDispatch|getMethodsMetaData|getPackageName|getRefClass|getSlots|getValidity|hasArg|hasLoadAction|hasMethod|hasMethods|implicitGeneric|inheritedSlotNames|initFieldArgs|initialize|initRefFields|insertClassMethods|insertMethod|insertSource|is|isClass|isClassDef|isClassUnion|isGeneric|isGrammarSymbol|isGroup|isRematched|isSealedClass|isSealedMethod|isVirtualClass|isXS3Class|kronecker|languageEl|linearizeMlist|listFromMethods|listFromMlist|loadMethod|Logic|makeClassRepresentation|makeExtends|makeGeneric|makeMethodsList|makePrototypeFromClassDef|makeStandardGeneric|matchSignature|Math|Math2|mergeMethods|metaNameUndo|MethodAddCoerce|methodSignatureMatrix|method\\.skeleton|MethodsList|MethodsListSelect|methodsPackageMetaName|missingArg|multipleClasses|new|newBasic|newClassRepresentation|newEmptyObject|Ops|packageSlot|possibleExtends|prohibitGeneric|promptClass|promptMethods|prototype|Quote|rbind2|reconcilePropertiesAndPrototype|registerImplicitGenerics|rematchDefinition|removeClass|removeGeneric|removeMethod|removeMethods|representation|requireMethods|resetClass|resetGeneric|S3Class|S3Part|sealClass|selectMethod|selectSuperClasses|setAs|setClass|setClassUnion|setDataPart|setGeneric|setGenericImplicit|setGroupGeneric|setIs|setLoadAction|setLoadActions|setMethod|setOldClass|setPackageName|setPrimitiveMethods|setRefClass|setReplaceMethod|setValidity|show|showClass|showDefault|showExtends|showMethods|showMlist|signature|SignatureMethod|sigToEnv|slot|slotNames|slotsFromS3|substituteDirect|substituteFunctionArgs|Summary|superClassDepth|testInheritedMethods|testVirtual|tryNew|unRematchDefinition|validObject|validSlotNames)\\s*(\\()", - "beginCaptures": { - "1": { - "name": "support.function.r" - }, - "2": { - "name": "punctuation.definition.arguments.begin.r" - } - }, - "contentName": "meta.function-call.arguments.r", - "end": "(\\))", - "endCaptures": { - "1": { - "name": "punctuation.definition.arguments.end.r" - } - }, - "name": "meta.function-call.r", - "patterns": [ - { - "include": "#function-call-arguments" - } - ] - }, - { - "begin": "\\b(acf|acf2AR|add1|addmargins|add\\.scope|aggregate|aggregate\\.data\\.frame|aggregate\\.ts|AIC|alias|anova|ansari\\.test|aov|approx|approxfun|ar|ar\\.burg|arima|arima0|arima0\\.diag|arima\\.sim|ARMAacf|ARMAtoMA|ar\\.mle|ar\\.ols|ar\\.yw|as\\.dendrogram|as\\.dist|as\\.formula|as\\.hclust|asOneSidedFormula|as\\.stepfun|as\\.ts|ave|bandwidth\\.kernel|bartlett\\.test|BIC|binomial|binom\\.test|biplot|Box\\.test|bw\\.bcv|bw\\.nrd|bw\\.nrd0|bw\\.SJ|bw\\.ucv|C|cancor|case\\.names|ccf|chisq\\.test|cmdscale|coef|coefficients|complete\\.cases|confint|confint\\.default|confint\\.lm|constrOptim|contrasts|contr\\.helmert|contr\\.poly|contr\\.SAS|contr\\.sum|contr\\.treatment|convolve|cooks\\.distance|cophenetic|cor|cor\\.test|cov|cov2cor|covratio|cov\\.wt|cpgram|cutree|cycle|D|dbeta|dbinom|dcauchy|dchisq|decompose|delete\\.response|deltat|dendrapply|density|density\\.default|deriv|deriv3|deviance|dexp|df|DF2formula|dfbeta|dfbetas|dffits|df\\.kernel|df\\.residual|dgamma|dgeom|dhyper|diffinv|dist|dlnorm|dlogis|dmultinom|dnbinom|dnorm|dpois|drop1|drop\\.scope|drop\\.terms|dsignrank|dt|dummy\\.coef|dummy\\.coef\\.lm|dunif|dweibull|dwilcox|ecdf|eff\\.aovlist|effects|embed|end|estVar|expand\\.model\\.frame|extractAIC|factanal|factor\\.scope|family|fft|filter|fisher\\.test|fitted|fitted\\.values|fivenum|fligner\\.test|formula|frequency|friedman\\.test|ftable|Gamma|gaussian|get_all_vars|getCall|getInitial|glm|glm\\.control|glm\\.fit|hasTsp|hat|hatvalues|hclust|heatmap|HoltWinters|influence|influence\\.measures|integrate|interaction\\.plot|inverse\\.gaussian|IQR|is\\.empty\\.model|is\\.leaf|is\\.mts|isoreg|is\\.stepfun|is\\.ts|is\\.tskernel|KalmanForecast|KalmanLike|KalmanRun|KalmanSmooth|kernapply|kernel|kmeans|knots|kruskal\\.test|ksmooth|ks\\.test|lag|lag\\.plot|line|lm|lm\\.fit|lm\\.influence|lm\\.wfit|loadings|loess|loess\\.control|loess\\.smooth|logLik|loglin|lowess|ls\\.diag|lsfit|ls\\.print|mad|mahalanobis|makeARIMA|make\\.link|makepredictcall|manova|mantelhaen\\.test|mauchly\\.test|mcnemar\\.test|median|median\\.default|medpolish|model\\.extract|model\\.frame|model\\.frame\\.default|model\\.matrix|model\\.matrix\\.default|model\\.matrix\\.lm|model\\.offset|model\\.response|model\\.tables|model\\.weights|monthplot|mood\\.test|mvfft|na\\.action|na\\.contiguous|na\\.exclude|na\\.fail|na\\.omit|na\\.pass|napredict|naprint|naresid|nextn|nlm|nlminb|nls|nls\\.control|NLSstAsymptotic|NLSstClosestX|NLSstLfAsymptote|NLSstRtAsymptote|nobs|numericDeriv|offset|oneway\\.test|optim|optimHess|optimise|optimize|order\\.dendrogram|pacf|p\\.adjust|p\\.adjust\\.methods|Pair|pairwise\\.prop\\.test|pairwise\\.table|pairwise\\.t\\.test|pairwise\\.wilcox\\.test|pbeta|pbinom|pbirthday|pcauchy|pchisq|pexp|pf|pgamma|pgeom|phyper|plclust|plnorm|plogis|plot\\.ecdf|plot\\.spec\\.coherency|plot\\.spec\\.phase|plot\\.stepfun|plot\\.ts|pnbinom|pnorm|poisson|poisson\\.test|poly|polym|power|power\\.anova\\.test|power\\.prop\\.test|power\\.t\\.test|ppoints|ppois|ppr|PP\\.test|prcomp|predict|predict\\.glm|predict\\.lm|preplot|princomp|printCoefmat|profile|proj|promax|prop\\.test|prop\\.trend\\.test|psignrank|pt|ptukey|punif|pweibull|pwilcox|qbeta|qbinom|qbirthday|qcauchy|qchisq|qexp|qf|qgamma|qgeom|qhyper|qlnorm|qlogis|qnbinom|qnorm|qpois|qqline|qqnorm|qqplot|qsignrank|qt|qtukey|quade\\.test|quantile|quasi|quasibinomial|quasipoisson|qunif|qweibull|qwilcox|r2dtable|rbeta|rbinom|rcauchy|rchisq|read\\.ftable|rect\\.hclust|reformulate|relevel|reorder|replications|reshape|resid|residuals|residuals\\.glm|residuals\\.lm|rexp|rf|rgamma|rgeom|rhyper|rlnorm|rlogis|rmultinom|rnbinom|rnorm|rpois|rsignrank|rstandard|rstudent|rt|runif|runmed|rweibull|rwilcox|rWishart|scatter\\.smooth|screeplot|sd|se\\.contrast|selfStart|setNames|shapiro\\.test|sigma|simulate|smooth|smoothEnds|smooth\\.spline|sortedXyData|spec\\.ar|spec\\.pgram|spec\\.taper|spectrum|spline|splinefun|splinefunH|SSasymp|SSasympOff|SSasympOrig|SSbiexp|SSD|SSfol|SSfpl|SSgompertz|SSlogis|SSmicmen|SSweibull|start|stat\\.anova|step|stepfun|stl|StructTS|summary\\.aov|summary\\.glm|summary\\.lm|summary\\.manova|summary\\.stepfun|supsmu|symnum|termplot|terms|terms\\.formula|time|toeplitz|ts|tsdiag|ts\\.intersect|tsp|ts\\.plot|tsSmooth|ts\\.union|t\\.test|TukeyHSD|uniroot|update|update\\.default|update\\.formula|var|variable\\.names|varimax|var\\.test|vcov|weighted\\.mean|weighted\\.residuals|weights|wilcox\\.test|window|write\\.ftable|xtabs)\\s*(\\()", - "beginCaptures": { - "1": { - "name": "support.function.r" - }, - "2": { - "name": "punctuation.definition.arguments.begin.r" - } - }, - "contentName": "meta.function-call.arguments.r", - "end": "(\\))", - "endCaptures": { - "1": { - "name": "punctuation.definition.arguments.end.r" - } - }, - "name": "meta.function-call.r", - "patterns": [ - { - "include": "#function-call-arguments" - } - ] - }, - { - "begin": "\\b(adist|alarm|apropos|aregexec|argsAnywhere|asDateBuilt|askYesNo|aspell|aspell_package_C_files|aspell_package_Rd_files|aspell_package_R_files|aspell_package_vignettes|aspell_write_personal_dictionary_file|as\\.person|as\\.personList|as\\.relistable|as\\.roman|assignInMyNamespace|assignInNamespace|available\\.packages|bibentry|browseEnv|browseURL|browseVignettes|bug\\.report|capture\\.output|changedFiles|charClass|checkCRAN|chooseBioCmirror|chooseCRANmirror|citation|cite|citeNatbib|citEntry|citFooter|citHeader|close\\.socket|combn|compareVersion|contrib\\.url|count\\.fields|create\\.post|data|dataentry|data\\.entry|de|debugcall|debugger|demo|de\\.ncols|de\\.restore|de\\.setup|download\\.file|download\\.packages|dump\\.frames|edit|emacs|example|file\\.edit|fileSnapshot|file_test|find|findLineNum|fix|fixInNamespace|flush\\.console|formatOL|formatUL|getAnywhere|getCRANmirrors|getFromNamespace|getParseData|getParseText|getS3method|getSrcDirectory|getSrcFilename|getSrcLocation|getSrcref|getTxtProgressBar|glob2rx|globalVariables|hasName|head|head\\.matrix|help|help\\.request|help\\.search|help\\.start|history|hsearch_db|hsearch_db_concepts|hsearch_db_keywords|installed\\.packages|install\\.packages|is\\.relistable|isS3method|isS3stdGeneric|limitedLabels|loadhistory|localeToCharset|lsf\\.str|ls\\.str|maintainer|make\\.packages\\.html|makeRweaveLatexCodeRunner|make\\.socket|memory\\.limit|memory\\.size|menu|methods|mirror2html|modifyList|new\\.packages|news|nsl|object\\.size|old\\.packages|osVersion|packageDate|packageDescription|packageName|package\\.skeleton|packageStatus|packageVersion|page|person|personList|pico|process\\.events|prompt|promptData|promptImport|promptPackage|rc\\.getOption|rc\\.options|rc\\.settings|rc\\.status|readCitationFile|read\\.csv|read\\.csv2|read\\.delim|read\\.delim2|read\\.DIF|read\\.fortran|read\\.fwf|read\\.socket|read\\.table|recover|relist|remove\\.packages|removeSource|Rprof|Rprofmem|RShowDoc|RSiteSearch|rtags|Rtangle|RtangleFinish|RtangleRuncode|RtangleSetup|RtangleWritedoc|RweaveChunkPrefix|RweaveEvalWithOpt|RweaveLatex|RweaveLatexFinish|RweaveLatexOptions|RweaveLatexSetup|RweaveLatexWritedoc|RweaveTryStop|savehistory|select\\.list|sessionInfo|setBreakpoint|setRepositories|setTxtProgressBar|stack|Stangle|str|strcapture|strOptions|summaryRprof|suppressForeignCheck|Sweave|SweaveHooks|SweaveSyntaxLatex|SweaveSyntaxNoweb|SweaveSyntConv|tail|tail\\.matrix|tar|timestamp|toBibtex|toLatex|txtProgressBar|type\\.convert|undebugcall|unstack|untar|unzip|update\\.packages|upgrade|URLdecode|URLencode|url\\.show|vi|View|vignette|warnErrList|write\\.csv|write\\.csv2|write\\.socket|write\\.table|xedit|xemacs|zip)\\s*(\\()", - "beginCaptures": { - "1": { - "name": "support.function.r" - }, - "2": { - "name": "punctuation.definition.arguments.begin.r" - } - }, - "contentName": "meta.function-call.arguments.r", - "end": "(\\))", - "endCaptures": { - "1": { - "name": "punctuation.definition.arguments.end.r" - } - }, - "name": "meta.function-call.r", - "patterns": [ - { - "include": "#function-call-arguments" + "match": "\\\\.", + "name": "constant.character.escape.r" } ] } diff --git a/extensions/references-view/README.md b/extensions/references-view/README.md index ba874165423..6a4f08bbf1f 100644 --- a/extensions/references-view/README.md +++ b/extensions/references-view/README.md @@ -1,11 +1,11 @@ # References View -This extension shows reference search results as separate view, just like search results. It complements the peek view presentation that is also built into VS Code. The following feature are available: +This extension shows reference search results as separate view, just like search results. It complements the peek view presentation that is also built into VS Code. The following features are available: -* List All References via the Command Palette, the Context Menu, or via Alt+Shift+F12 -* View references in a dedicated tree view that sits in the sidebar -* Navigate through search results via F4 and Shift+F4 -* Remove references from the list via inline commands +- List All References via the Command Palette, the Context Menu, or via Alt+Shift+F12 +- View references in a dedicated tree view that sits in the sidebar +- Navigate through search results via F4 and Shift+F4 +- Remove references from the list via inline commands ![](https://raw.githubusercontent.com/microsoft/vscode-references-view/master/media/demo.png) @@ -21,7 +21,7 @@ This extension ships with Visual Studio Code and uses its issue tracker. Please # Contributing -This project welcomes contributions and suggestions. Most contributions require you to agree to a +This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit https://cla.microsoft.com. diff --git a/extensions/references-view/package-lock.json b/extensions/references-view/package-lock.json index bb5c5e10e1f..fe0d8aad7de 100644 --- a/extensions/references-view/package-lock.json +++ b/extensions/references-view/package-lock.json @@ -9,26 +9,28 @@ "version": "1.0.0", "license": "MIT", "devDependencies": { - "@types/node": "20.x" + "@types/node": "22.x" }, "engines": { "vscode": "^1.67.0" } }, "node_modules/@types/node": { - "version": "20.11.24", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.24.tgz", - "integrity": "sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long==", + "version": "22.13.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", + "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.20.0" } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" } } } diff --git a/extensions/references-view/package.json b/extensions/references-view/package.json index 9566a965c76..62c9e29e0c6 100644 --- a/extensions/references-view/package.json +++ b/extensions/references-view/package.json @@ -399,6 +399,6 @@ "watch": "npx gulp watch-extension:references-view" }, "devDependencies": { - "@types/node": "20.x" + "@types/node": "22.x" } } diff --git a/extensions/terminal-suggest/src/env/pathExecutableCache.ts b/extensions/terminal-suggest/src/env/pathExecutableCache.ts index 9932bc88933..ae7686865af 100644 --- a/extensions/terminal-suggest/src/env/pathExecutableCache.ts +++ b/extensions/terminal-suggest/src/env/pathExecutableCache.ts @@ -12,6 +12,7 @@ import { getFriendlyResourcePath } from '../helpers/uri'; import { SettingsIds } from '../constants'; import * as filesystem from 'fs'; import * as path from 'path'; +import { TerminalShellType } from '../terminalSuggestMain'; const isWindows = osIsWindows(); @@ -45,10 +46,14 @@ export class PathExecutableCache implements vscode.Disposable { this._cachedPathValue = undefined; } - async getExecutablesInPath(env: ITerminalEnvironment = process.env): Promise<{ completionResources: Set | undefined; labels: Set | undefined } | undefined> { + async getExecutablesInPath(env: ITerminalEnvironment = process.env, shellType?: TerminalShellType): Promise<{ completionResources: Set | undefined; labels: Set | undefined } | undefined> { // Create cache key let pathValue: string | undefined; - if (isWindows) { + if (shellType === TerminalShellType.GitBash) { + // TODO: figure out why shellIntegration.env.PATH + // regressed from using \ to / (correct) + pathValue = process.env.PATH; + } else if (isWindows) { const caseSensitivePathKey = Object.keys(env).find(key => key.toLowerCase() === 'path'); if (caseSensitivePathKey) { pathValue = env[caseSensitivePathKey]; diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts index 382142f8aa3..736a064ed21 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -105,7 +105,7 @@ export async function activate(context: vscode.ExtensionContext) { return; } - const commandsInPath = await pathExecutableCache.getExecutablesInPath(terminal.shellIntegration?.env?.value); + const commandsInPath = await pathExecutableCache.getExecutablesInPath(terminal.shellIntegration?.env?.value, terminalShellType); const shellGlobals = await getShellGlobals(terminalShellType, commandsInPath?.labels) ?? []; if (!commandsInPath?.completionResources) { console.debug('#terminalCompletions No commands found in path'); @@ -143,7 +143,7 @@ export async function activate(context: vscode.ExtensionContext) { } if (terminal.shellIntegration?.cwd && (result.filesRequested || result.foldersRequested)) { - return new vscode.TerminalCompletionList(result.items, { filesRequested: result.filesRequested, foldersRequested: result.foldersRequested, fileExtensions: result.fileExtensions, cwd: result.cwd ?? terminal.shellIntegration.cwd, env: terminal.shellIntegration?.env?.value }); + return new vscode.TerminalCompletionList(result.items, { filesRequested: result.filesRequested, foldersRequested: result.foldersRequested, fileExtensions: result.fileExtensions, cwd: result.cwd ?? terminal.shellIntegration.cwd, env: terminal.shellIntegration?.env?.value, }); } return result.items; } diff --git a/extensions/tunnel-forwarding/package-lock.json b/extensions/tunnel-forwarding/package-lock.json index 307cef66071..b62fef9300e 100644 --- a/extensions/tunnel-forwarding/package-lock.json +++ b/extensions/tunnel-forwarding/package-lock.json @@ -9,26 +9,26 @@ "version": "1.0.0", "license": "MIT", "devDependencies": { - "@types/node": "20.x" + "@types/node": "22.x" }, "engines": { "vscode": "^1.82.0" } }, "node_modules/@types/node": { - "version": "20.17.27", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.27.tgz", - "integrity": "sha512-U58sbKhDrthHlxHRJw7ZLiLDZGmAUOZUbpw0S6nL27sYUdhvgBLCRu/keSd6qcTsfArd1sRFCCBxzWATGr/0UA==", + "version": "22.13.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", + "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.19.2" + "undici-types": "~6.20.0" } }, "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "dev": true, "license": "MIT" } diff --git a/extensions/tunnel-forwarding/package.json b/extensions/tunnel-forwarding/package.json index 315baa03598..6928b1a0815 100644 --- a/extensions/tunnel-forwarding/package.json +++ b/extensions/tunnel-forwarding/package.json @@ -44,7 +44,7 @@ "watch": "gulp watch-extension:tunnel-forwarding" }, "devDependencies": { - "@types/node": "20.x" + "@types/node": "22.x" }, "prettier": { "printWidth": 100, diff --git a/extensions/typescript-language-features/package-lock.json b/extensions/typescript-language-features/package-lock.json index 2078b4845e8..f0f86a349f7 100644 --- a/extensions/typescript-language-features/package-lock.json +++ b/extensions/typescript-language-features/package-lock.json @@ -20,7 +20,7 @@ "vscode-uri": "^3.0.3" }, "devDependencies": { - "@types/node": "20.x", + "@types/node": "22.x", "@types/semver": "^5.5.0" }, "engines": { @@ -152,13 +152,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.17.27", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.27.tgz", - "integrity": "sha512-U58sbKhDrthHlxHRJw7ZLiLDZGmAUOZUbpw0S6nL27sYUdhvgBLCRu/keSd6qcTsfArd1sRFCCBxzWATGr/0UA==", + "version": "22.13.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", + "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.19.2" + "undici-types": "~6.20.0" } }, "node_modules/@types/semver": { @@ -258,9 +258,9 @@ "integrity": "sha512-V+uqV66BOQnWxvI6HjDnE4VkInmYZUQ4dgB7gzaDyFyFSK1i1nF/j7DpS9UbQAgV9NaF1XpcyuavnM1qOeiEIg==" }, "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "dev": true, "license": "MIT" }, diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index e7ed0c95ab5..a087e162080 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -49,7 +49,7 @@ "vscode-uri": "^3.0.3" }, "devDependencies": { - "@types/node": "20.x", + "@types/node": "22.x", "@types/semver": "^5.5.0" }, "scripts": { diff --git a/extensions/typescript-language-features/src/languageFeatures/quickFix.ts b/extensions/typescript-language-features/src/languageFeatures/quickFix.ts index f724cfd8c44..8da3ab3ee92 100644 --- a/extensions/typescript-language-features/src/languageFeatures/quickFix.ts +++ b/extensions/typescript-language-features/src/languageFeatures/quickFix.ts @@ -450,7 +450,7 @@ class TypeScriptQuickFixProvider implements vscode.CodeActionProvider { if (x === diagnostic) { return false; @@ -479,7 +479,7 @@ class TypeScriptQuickFixProvider implements vscode.CodeActionProvider([ // Missing async diff --git a/extensions/typescript-language-features/src/logging/telemetry.ts b/extensions/typescript-language-features/src/logging/telemetry.ts index b96487b2419..61d13f93552 100644 --- a/extensions/typescript-language-features/src/logging/telemetry.ts +++ b/extensions/typescript-language-features/src/logging/telemetry.ts @@ -36,10 +36,10 @@ export class VSCodeTelemetryReporter implements TelemetryReporter { reporter.postEventObj(eventName, properties); } - public logTraceEvent(point: string, id: string, data?: string): void { - const event: { point: string; id: string; data?: string | undefined } = { + public logTraceEvent(point: string, traceId: string, data?: string): void { + const event: { point: string; traceId: string; data?: string | undefined } = { point, - id + traceId }; if (data) { event.data = data; @@ -52,7 +52,7 @@ export class VSCodeTelemetryReporter implements TelemetryReporter { "${TypeScriptCommonProperties}" ], "point" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The trace point." }, - "id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The traceId is used to correlate the request with other trace points." }, + "traceId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The traceId is used to correlate the request with other trace points." }, "data": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Additional data" } } */ diff --git a/extensions/typescript-language-features/src/tsServer/server.ts b/extensions/typescript-language-features/src/tsServer/server.ts index dbb867f8bb3..cce320d3997 100644 --- a/extensions/typescript-language-features/src/tsServer/server.ts +++ b/extensions/typescript-language-features/src/tsServer/server.ts @@ -271,7 +271,8 @@ export class SingleTsServer extends Disposable implements ITypeScriptServer { this._requestQueue.enqueue(requestInfo); if (args && typeof (args as any).$traceId === 'string') { const queueLength = this._requestQueue.length - 1; - this._telemetryReporter.logTraceEvent('TSServer.enqueueRequest', (args as any).$traceId, JSON.stringify({ command, queueLength })); + const pendingResponses = this._pendingResponses.size; + this._telemetryReporter.logTraceEvent('TSServer.enqueueRequest', (args as any).$traceId, JSON.stringify({ command, queueLength, pendingResponses })); } this.sendNextRequests(); diff --git a/extensions/vscode-api-tests/package-lock.json b/extensions/vscode-api-tests/package-lock.json index 9a80cf4f19b..204a2f14655 100644 --- a/extensions/vscode-api-tests/package-lock.json +++ b/extensions/vscode-api-tests/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "devDependencies": { "@types/mocha": "^9.1.1", - "@types/node": "20.x", + "@types/node": "22.x", "@types/node-forge": "^1.3.11", "node-forge": "^1.3.1", "straightforward": "^4.2.2" @@ -26,13 +26,13 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.17.27", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.27.tgz", - "integrity": "sha512-U58sbKhDrthHlxHRJw7ZLiLDZGmAUOZUbpw0S6nL27sYUdhvgBLCRu/keSd6qcTsfArd1sRFCCBxzWATGr/0UA==", + "version": "22.13.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", + "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.19.2" + "undici-types": "~6.20.0" } }, "node_modules/@types/node-forge": { @@ -217,9 +217,9 @@ } }, "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "dev": true, "license": "MIT" }, diff --git a/extensions/vscode-api-tests/package.json b/extensions/vscode-api-tests/package.json index 0d7f87a9be4..440d4953a28 100644 --- a/extensions/vscode-api-tests/package.json +++ b/extensions/vscode-api-tests/package.json @@ -27,7 +27,6 @@ "interactive", "languageStatusText", "nativeWindowHandle", - "notebookCellExecutionState", "notebookDeprecated", "notebookLiveShare", "notebookMessaging", @@ -251,7 +250,7 @@ }, "devDependencies": { "@types/mocha": "^9.1.1", - "@types/node": "20.x", + "@types/node": "22.x", "@types/node-forge": "^1.3.11", "node-forge": "^1.3.1", "straightforward": "^4.2.2" diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.kernel.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.kernel.test.ts index 37e16207ddb..d1fafae7591 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.kernel.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.kernel.test.ts @@ -278,38 +278,6 @@ const apiTestSerializer: vscode.NotebookSerializer = { }); }); - test('onDidChangeCellExecutionState is fired', async () => { - const notebook = await openRandomNotebookDocument(); - const editor = await vscode.window.showNotebookDocument(notebook); - const cell = editor.notebook.cellAt(0); - - let eventCount = 0; - const def = new DeferredPromise(); - testDisposables.push(vscode.notebooks.onDidChangeNotebookCellExecutionState(e => { - try { - assert.strictEqual(e.cell.document.uri.toString(), cell.document.uri.toString(), 'event should be fired for the executing cell'); - - if (eventCount === 0) { - assert.strictEqual(e.state, vscode.NotebookCellExecutionState.Pending, 'should be set to Pending'); - } else if (eventCount === 1) { - assert.strictEqual(e.state, vscode.NotebookCellExecutionState.Executing, 'should be set to Executing'); - assert.strictEqual(cell.outputs.length, 0, 'no outputs yet: ' + JSON.stringify(cell.outputs[0])); - } else if (e.state === vscode.NotebookCellExecutionState.Idle) { - assert.strictEqual(cell.outputs.length, 1, 'should have an output'); - def.complete(); - } - - eventCount++; - } catch (err) { - def.error(err); - } - })); - - vscode.commands.executeCommand('notebook.cell.execute', { document: notebook.uri, ranges: [{ start: 0, end: 1 }] }); - - await def.p; - }); - test('Output changes are applied once the promise resolves', async function () { let called = false; diff --git a/extensions/vscode-colorize-perf-tests/package-lock.json b/extensions/vscode-colorize-perf-tests/package-lock.json index b0516cc1e28..b56d09c9a0c 100644 --- a/extensions/vscode-colorize-perf-tests/package-lock.json +++ b/extensions/vscode-colorize-perf-tests/package-lock.json @@ -12,19 +12,20 @@ "jsonc-parser": "^3.2.0" }, "devDependencies": { - "@types/node": "20.x" + "@types/node": "22.x" }, "engines": { "vscode": "*" } }, "node_modules/@types/node": { - "version": "20.11.24", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.24.tgz", - "integrity": "sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long==", + "version": "22.13.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", + "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.20.0" } }, "node_modules/jsonc-parser": { @@ -33,10 +34,11 @@ "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==" }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" } } } diff --git a/extensions/vscode-colorize-perf-tests/package.json b/extensions/vscode-colorize-perf-tests/package.json index 2e72152e284..fb7ce45f613 100644 --- a/extensions/vscode-colorize-perf-tests/package.json +++ b/extensions/vscode-colorize-perf-tests/package.json @@ -22,7 +22,7 @@ "jsonc-parser": "^3.2.0" }, "devDependencies": { - "@types/node": "20.x" + "@types/node": "22.x" }, "repository": { "type": "git", diff --git a/extensions/vscode-colorize-tests/package-lock.json b/extensions/vscode-colorize-tests/package-lock.json index 4011d73bcac..1aee98f66ee 100644 --- a/extensions/vscode-colorize-tests/package-lock.json +++ b/extensions/vscode-colorize-tests/package-lock.json @@ -12,19 +12,20 @@ "jsonc-parser": "^3.2.0" }, "devDependencies": { - "@types/node": "20.x" + "@types/node": "22.x" }, "engines": { "vscode": "*" } }, "node_modules/@types/node": { - "version": "20.11.24", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.24.tgz", - "integrity": "sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long==", + "version": "22.13.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", + "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.20.0" } }, "node_modules/jsonc-parser": { @@ -33,10 +34,11 @@ "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==" }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" } } } diff --git a/extensions/vscode-colorize-tests/package.json b/extensions/vscode-colorize-tests/package.json index b416aeee103..49592763745 100644 --- a/extensions/vscode-colorize-tests/package.json +++ b/extensions/vscode-colorize-tests/package.json @@ -22,7 +22,7 @@ "jsonc-parser": "^3.2.0" }, "devDependencies": { - "@types/node": "20.x" + "@types/node": "22.x" }, "contributes": { "semanticTokenTypes": [ diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_r.json b/extensions/vscode-colorize-tests/test/colorize-results/test_r.json index 87b66e78daf..4d56b651660 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_r.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_r.json @@ -561,7 +561,7 @@ }, { "c": "{", - "t": "source.r punctuation.section.braces.begin.r", + "t": "source.r punctuation.section.block.begin.bracket.curly.r", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -617,7 +617,7 @@ }, { "c": "}", - "t": "source.r punctuation.section.braces.end.r", + "t": "source.r punctuation.section.block.end.bracket.curly.r", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", diff --git a/extensions/vscode-test-resolver/package-lock.json b/extensions/vscode-test-resolver/package-lock.json index 3bdbeac9b3e..3829b4ea05e 100644 --- a/extensions/vscode-test-resolver/package-lock.json +++ b/extensions/vscode-test-resolver/package-lock.json @@ -9,26 +9,26 @@ "version": "0.0.1", "license": "MIT", "devDependencies": { - "@types/node": "20.x" + "@types/node": "22.x" }, "engines": { "vscode": "^1.25.0" } }, "node_modules/@types/node": { - "version": "20.17.27", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.27.tgz", - "integrity": "sha512-U58sbKhDrthHlxHRJw7ZLiLDZGmAUOZUbpw0S6nL27sYUdhvgBLCRu/keSd6qcTsfArd1sRFCCBxzWATGr/0UA==", + "version": "22.13.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", + "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.19.2" + "undici-types": "~6.20.0" } }, "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "dev": true, "license": "MIT" } diff --git a/extensions/vscode-test-resolver/package.json b/extensions/vscode-test-resolver/package.json index 8ab2171ddaa..c96c1d5894f 100644 --- a/extensions/vscode-test-resolver/package.json +++ b/extensions/vscode-test-resolver/package.json @@ -33,7 +33,7 @@ "main": "./out/extension", "browser": "./dist/browser/testResolverMain", "devDependencies": { - "@types/node": "20.x" + "@types/node": "22.x" }, "capabilities": { "untrustedWorkspaces": { diff --git a/package-lock.json b/package-lock.json index f9ab9254bc0..4d3e8598e0b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,16 +27,16 @@ "@vscode/windows-mutex": "^0.5.0", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-clipboard": "^0.2.0-beta.84", - "@xterm/addon-image": "^0.9.0-beta.101", - "@xterm/addon-ligatures": "^0.10.0-beta.101", - "@xterm/addon-progress": "^0.2.0-beta.7", - "@xterm/addon-search": "^0.16.0-beta.101", - "@xterm/addon-serialize": "^0.14.0-beta.101", - "@xterm/addon-unicode11": "^0.9.0-beta.101", - "@xterm/addon-webgl": "^0.19.0-beta.101", - "@xterm/headless": "^5.6.0-beta.101", - "@xterm/xterm": "^5.6.0-beta.101", + "@xterm/addon-clipboard": "^0.2.0-beta.90", + "@xterm/addon-image": "^0.9.0-beta.107", + "@xterm/addon-ligatures": "^0.10.0-beta.107", + "@xterm/addon-progress": "^0.2.0-beta.13", + "@xterm/addon-search": "^0.16.0-beta.107", + "@xterm/addon-serialize": "^0.14.0-beta.107", + "@xterm/addon-unicode11": "^0.9.0-beta.107", + "@xterm/addon-webgl": "^0.19.0-beta.107", + "@xterm/headless": "^5.6.0-beta.107", + "@xterm/xterm": "^5.6.0-beta.107", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", "jschardet": "3.1.4", @@ -66,7 +66,7 @@ "@types/kerberos": "^1.1.2", "@types/minimist": "^1.2.1", "@types/mocha": "^9.1.1", - "@types/node": "20.x", + "@types/node": "22.x", "@types/sinon": "^10.0.2", "@types/sinon-test": "^2.4.2", "@types/trusted-types": "^1.0.6", @@ -96,7 +96,7 @@ "cssnano": "^6.0.3", "debounce": "^1.0.0", "deemon": "^1.13.4", - "electron": "34.5.1", + "electron": "35.2.2", "eslint": "^9.11.1", "eslint-formatter-compact": "^8.40.0", "eslint-plugin-header": "3.1.1", @@ -2292,13 +2292,13 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.17.27", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.27.tgz", - "integrity": "sha512-U58sbKhDrthHlxHRJw7ZLiLDZGmAUOZUbpw0S6nL27sYUdhvgBLCRu/keSd6qcTsfArd1sRFCCBxzWATGr/0UA==", + "version": "22.13.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", + "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.19.2" + "undici-types": "~6.20.0" } }, "node_modules/@types/node-fetch": { @@ -3312,30 +3312,30 @@ } }, "node_modules/@xterm/addon-clipboard": { - "version": "0.2.0-beta.84", - "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.84.tgz", - "integrity": "sha512-/7lRpyLboTDKa1SMQCkLkUnH5hawiDsZ1VDMhfgjEr44ltw3cv2YuTtPQYkKen0vfu/0uzZeHWCwsZpQK25nRA==", + "version": "0.2.0-beta.90", + "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.90.tgz", + "integrity": "sha512-2i7qtACRBYRTRba831ufEjQMeAvK4uuVPYPBkSXzKJ/dIAvhws5B6OOmxqZzR97OsmzUC/SrjHSujgwyD0MVVw==", "license": "MIT", "dependencies": { "js-base64": "^3.7.5" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.101" + "@xterm/xterm": "^5.6.0-beta.107" } }, "node_modules/@xterm/addon-image": { - "version": "0.9.0-beta.101", - "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.101.tgz", - "integrity": "sha512-iAp4DFxqEhN1DWcCy3d66NgrAklKXfZhHlE8T0rvGS1mfK8ubO5WODXUdMO0rwU5TSrnt4l21DVwFhSs+2oWQw==", + "version": "0.9.0-beta.107", + "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.107.tgz", + "integrity": "sha512-/vqb2BhVjJeCcOIDSpjWrfXbhNTCse95qkKbpmzFkQJJEh0CyaqjSUBIAM+Qcl9f/x6H//O2wADnn3Wsn5qYsg==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.101" + "@xterm/xterm": "^5.6.0-beta.107" } }, "node_modules/@xterm/addon-ligatures": { - "version": "0.10.0-beta.101", - "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.101.tgz", - "integrity": "sha512-QgixJpyzP4ZFhv0YJJgNFXih7escNod9cGTAG7eW/dYwnunZwSmi7Bal/u3m6IC5SZbjAAOjKBGZyfvHefK7SA==", + "version": "0.10.0-beta.107", + "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.107.tgz", + "integrity": "sha512-3qQZz6dPS8XlGGwz1p5Bqjoosah6Km1GwGeNv7YxkCVU2kpTOymFH8BMUEOFC5WHdYseDXnz8hIEleexF9Pa2g==", "license": "MIT", "dependencies": { "font-finder": "^1.1.0", @@ -3345,64 +3345,64 @@ "node": ">8.0.0" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.101" + "@xterm/xterm": "^5.6.0-beta.107" } }, "node_modules/@xterm/addon-progress": { - "version": "0.2.0-beta.7", - "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.2.0-beta.7.tgz", - "integrity": "sha512-1FrJcHm2R+s7auGTrb3rzTevFz5nTP8dLHmY24iVq1a3rPxrprCkfDkugQJsCNG0rd5GT1qk9YWjVcu3GO7gQw==", + "version": "0.2.0-beta.13", + "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.2.0-beta.13.tgz", + "integrity": "sha512-BLmX+JdA+LLOMbVLQm+gPqieeKaVMiugBFpCtQtBus3kCs/2/F8pU/XNGV31CnXxDdRjptyOGrPVRreF/rDdcg==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.101" + "@xterm/xterm": "^5.6.0-beta.107" } }, "node_modules/@xterm/addon-search": { - "version": "0.16.0-beta.101", - "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.101.tgz", - "integrity": "sha512-rrT9KQsQb/OUQwSVvAIKNFslEM2ux6824GZYPB6uYJbFkRwI+aGKiqs8UM264UcZotHylMSg3dYybGPBImTH3A==", + "version": "0.16.0-beta.107", + "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.107.tgz", + "integrity": "sha512-poHjeKjnTKtS6rHyu3r5lhW/0rdLcRMSJLDmT9xo9lfTuyhfcUl8gro44iPtscKY+LEtrhZcortTlElMewy/Bw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.101" + "@xterm/xterm": "^5.6.0-beta.107" } }, "node_modules/@xterm/addon-serialize": { - "version": "0.14.0-beta.101", - "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.101.tgz", - "integrity": "sha512-LOJtJroDjoHY9EhSAr0UuWZ59bnZFnZ73xvBT0AyEH0Oqd7MC0LZtI0oV4ifcQU90Eb1oDq3LRfgHm9vAtUrFg==", + "version": "0.14.0-beta.107", + "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.107.tgz", + "integrity": "sha512-MGaoO2zlNuSGofX5Xfbw+MU2wgAApEsDOPeYoEBSQyLf5BR7Z2rGVCxFyF5zNsIhNDov7ptQIVpCfonMVAgUvg==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.101" + "@xterm/xterm": "^5.6.0-beta.107" } }, "node_modules/@xterm/addon-unicode11": { - "version": "0.9.0-beta.101", - "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.101.tgz", - "integrity": "sha512-iu1Ry7im8NO3hITbYHbsxZKTxiJQSvg/tGR1EXK1lFIXe9gHc6bqTQPhvFYZ8xgPNt+V1AHZY9SpwcxgBOuxUA==", + "version": "0.9.0-beta.107", + "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.107.tgz", + "integrity": "sha512-XTfJ1G7u+8zOT6faCfEB49AbVFAX4XWUoiLtyXCfRvbrVuv5wv53T03dLtBtpwjWoFaveLKGl3ZiU7t3dpl0RA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.101" + "@xterm/xterm": "^5.6.0-beta.107" } }, "node_modules/@xterm/addon-webgl": { - "version": "0.19.0-beta.101", - "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.101.tgz", - "integrity": "sha512-g9YzOEqYS7MW1QirNRQhUsRJeFKxsksVQ6iT1dOScjZg7DRwil7/HNS03hQkgigW2Ku3hP5hK0WdXDe4np20gQ==", + "version": "0.19.0-beta.107", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.107.tgz", + "integrity": "sha512-wdUkYiV4PnR9+7+1giJoFm5xrguCOKTH4xdQ/6lYuSbgMS0y1ykpcQjDNshwHy0TyUuh4o4z1+rd48qNjqGM6Q==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.101" + "@xterm/xterm": "^5.6.0-beta.107" } }, "node_modules/@xterm/headless": { - "version": "5.6.0-beta.101", - "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-5.6.0-beta.101.tgz", - "integrity": "sha512-uAIo1b50keq3Ybps3Q5QcakVz570hY7gdU/71v52N2BxbvXy0wPk92Q4lCapsKmdtQ3+HbLtRsh1s4k0oP4VGw==", + "version": "5.6.0-beta.107", + "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-5.6.0-beta.107.tgz", + "integrity": "sha512-jFoJEPXjffcSzQyf/fTh3eZsXuHP/xQmkezkKbmrkzxw5h40NPbTPQbhTrsUCea2M12Mun1BrT8ScSYMjUCH3w==", "license": "MIT" }, "node_modules/@xterm/xterm": { - "version": "5.6.0-beta.101", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.101.tgz", - "integrity": "sha512-kV6Ad/KTcCgKWTYshufBEfT3OyadFLuskW+R+nJIJKrlAB34vRsX7TXFJ0P9QoMAeqXQpgngDfTn+RTAESyVyw==", + "version": "5.6.0-beta.107", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.107.tgz", + "integrity": "sha512-7cuJFZtc7Rv9BEpf9UsvErfXhLKkEmogI0mizgri+nNjEENnpQcFOpx84GFTjax/Zta8MAxOMUP1XU5Guju80A==", "license": "MIT" }, "node_modules/@xtuc/ieee754": { @@ -6002,15 +6002,15 @@ "dev": true }, "node_modules/electron": { - "version": "34.5.1", - "resolved": "https://registry.npmjs.org/electron/-/electron-34.5.1.tgz", - "integrity": "sha512-z2Wm7QjhnJ5592fLITynj8UwIk1mBiT402mOakxSYiADrERIci3IOPk7xWHAFOMvt/eoG5RW16PPhgJiedZcGA==", + "version": "35.2.2", + "resolved": "https://registry.npmjs.org/electron/-/electron-35.2.2.tgz", + "integrity": "sha512-jZnCOtIgrt28esWP5z/PKndj/vPQ/Zt+cvNRlb/qOGnK/AjW1mASwPMtQ099NlSodf69RR3JrhnZCYbTWeDR4g==", "dev": true, "hasInstallScript": true, "license": "MIT", "dependencies": { "@electron/get": "^2.0.0", - "@types/node": "^20.9.0", + "@types/node": "^22.7.7", "extract-zip": "^2.0.1" }, "bin": { @@ -17624,9 +17624,9 @@ } }, "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "dev": true, "license": "MIT" }, diff --git a/package.json b/package.json index 2bf028a2cdb..4e2c8a75e1b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.101.0", - "distro": "3bfa1445f8f520d5a0e88723e980c828b404520e", + "distro": "d99bdfad25d9b7a48bd51d251c79ebbd6debda14", "author": { "name": "Microsoft Corporation" }, @@ -45,7 +45,7 @@ "tsec-compile-check": "node node_modules/tsec/bin/tsec -p src/tsconfig.tsec.json", "vscode-dts-compile-check": "tsc -p src/tsconfig.vscode-dts.json && tsc -p src/tsconfig.vscode-proposed-dts.json", "valid-layers-check": "node build/lib/layersChecker.js", - "property-init-order-check": "node build/lib/propertyInitOrderChecker.js", + "define-class-fields-check": "node build/lib/propertyInitOrderChecker.js && tsc -p src/tsconfig.defineClassFields.json", "update-distro": "node build/npm/update-distro.mjs", "web": "echo 'npm run web' is replaced by './scripts/code-server' or './scripts/code-web'", "compile-cli": "gulp compile-cli", @@ -86,16 +86,16 @@ "@vscode/windows-mutex": "^0.5.0", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-clipboard": "^0.2.0-beta.84", - "@xterm/addon-image": "^0.9.0-beta.101", - "@xterm/addon-ligatures": "^0.10.0-beta.101", - "@xterm/addon-progress": "^0.2.0-beta.7", - "@xterm/addon-search": "^0.16.0-beta.101", - "@xterm/addon-serialize": "^0.14.0-beta.101", - "@xterm/addon-unicode11": "^0.9.0-beta.101", - "@xterm/addon-webgl": "^0.19.0-beta.101", - "@xterm/headless": "^5.6.0-beta.101", - "@xterm/xterm": "^5.6.0-beta.101", + "@xterm/addon-clipboard": "^0.2.0-beta.90", + "@xterm/addon-image": "^0.9.0-beta.107", + "@xterm/addon-ligatures": "^0.10.0-beta.107", + "@xterm/addon-progress": "^0.2.0-beta.13", + "@xterm/addon-search": "^0.16.0-beta.107", + "@xterm/addon-serialize": "^0.14.0-beta.107", + "@xterm/addon-unicode11": "^0.9.0-beta.107", + "@xterm/addon-webgl": "^0.19.0-beta.107", + "@xterm/headless": "^5.6.0-beta.107", + "@xterm/xterm": "^5.6.0-beta.107", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", "jschardet": "3.1.4", @@ -125,7 +125,7 @@ "@types/kerberos": "^1.1.2", "@types/minimist": "^1.2.1", "@types/mocha": "^9.1.1", - "@types/node": "20.x", + "@types/node": "22.x", "@types/sinon": "^10.0.2", "@types/sinon-test": "^2.4.2", "@types/trusted-types": "^1.0.6", @@ -155,7 +155,7 @@ "cssnano": "^6.0.3", "debounce": "^1.0.0", "deemon": "^1.13.4", - "electron": "34.5.1", + "electron": "35.2.2", "eslint": "^9.11.1", "eslint-formatter-compact": "^8.40.0", "eslint-plugin-header": "3.1.1", diff --git a/product.json b/product.json index ba02ba293ba..91140d9a483 100644 --- a/product.json +++ b/product.json @@ -52,8 +52,8 @@ }, { "name": "ms-vscode.js-debug", - "version": "1.100.0", - "sha256": "69c746edf41508510818991e1d1a7cbac9d474dd666595540163d3cab435aeb9", + "version": "1.100.1", + "sha256": "8c2218df3422d45b95e96d9d28cdc4aa4426a2799aaaedd862d3f60ecab03844", "repo": "https://github.com/microsoft/vscode-js-debug", "metadata": { "id": "25629058-ddac-4e17-abba-74678e126c5d", diff --git a/remote/.npmrc b/remote/.npmrc index 3f17dea3fd9..a19943b3863 100644 --- a/remote/.npmrc +++ b/remote/.npmrc @@ -1,6 +1,6 @@ disturl="https://nodejs.org/dist" -target="20.19.0" -ms_build_id="332907" +target="22.14.0" +ms_build_id="329400" runtime="node" build_from_source="true" legacy-peer-deps="true" diff --git a/remote/package-lock.json b/remote/package-lock.json index a99aa799f69..309164505c3 100644 --- a/remote/package-lock.json +++ b/remote/package-lock.json @@ -20,16 +20,16 @@ "@vscode/vscode-languagedetection": "1.0.21", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-clipboard": "^0.2.0-beta.84", - "@xterm/addon-image": "^0.9.0-beta.101", - "@xterm/addon-ligatures": "^0.10.0-beta.101", - "@xterm/addon-progress": "^0.2.0-beta.7", - "@xterm/addon-search": "^0.16.0-beta.101", - "@xterm/addon-serialize": "^0.14.0-beta.101", - "@xterm/addon-unicode11": "^0.9.0-beta.101", - "@xterm/addon-webgl": "^0.19.0-beta.101", - "@xterm/headless": "^5.6.0-beta.101", - "@xterm/xterm": "^5.6.0-beta.101", + "@xterm/addon-clipboard": "^0.2.0-beta.90", + "@xterm/addon-image": "^0.9.0-beta.107", + "@xterm/addon-ligatures": "^0.10.0-beta.107", + "@xterm/addon-progress": "^0.2.0-beta.13", + "@xterm/addon-search": "^0.16.0-beta.107", + "@xterm/addon-serialize": "^0.14.0-beta.107", + "@xterm/addon-unicode11": "^0.9.0-beta.107", + "@xterm/addon-webgl": "^0.19.0-beta.107", + "@xterm/headless": "^5.6.0-beta.107", + "@xterm/xterm": "^5.6.0-beta.107", "cookie": "^0.7.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", @@ -523,30 +523,30 @@ "hasInstallScript": true }, "node_modules/@xterm/addon-clipboard": { - "version": "0.2.0-beta.84", - "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.84.tgz", - "integrity": "sha512-/7lRpyLboTDKa1SMQCkLkUnH5hawiDsZ1VDMhfgjEr44ltw3cv2YuTtPQYkKen0vfu/0uzZeHWCwsZpQK25nRA==", + "version": "0.2.0-beta.90", + "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.90.tgz", + "integrity": "sha512-2i7qtACRBYRTRba831ufEjQMeAvK4uuVPYPBkSXzKJ/dIAvhws5B6OOmxqZzR97OsmzUC/SrjHSujgwyD0MVVw==", "license": "MIT", "dependencies": { "js-base64": "^3.7.5" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.101" + "@xterm/xterm": "^5.6.0-beta.107" } }, "node_modules/@xterm/addon-image": { - "version": "0.9.0-beta.101", - "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.101.tgz", - "integrity": "sha512-iAp4DFxqEhN1DWcCy3d66NgrAklKXfZhHlE8T0rvGS1mfK8ubO5WODXUdMO0rwU5TSrnt4l21DVwFhSs+2oWQw==", + "version": "0.9.0-beta.107", + "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.107.tgz", + "integrity": "sha512-/vqb2BhVjJeCcOIDSpjWrfXbhNTCse95qkKbpmzFkQJJEh0CyaqjSUBIAM+Qcl9f/x6H//O2wADnn3Wsn5qYsg==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.101" + "@xterm/xterm": "^5.6.0-beta.107" } }, "node_modules/@xterm/addon-ligatures": { - "version": "0.10.0-beta.101", - "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.101.tgz", - "integrity": "sha512-QgixJpyzP4ZFhv0YJJgNFXih7escNod9cGTAG7eW/dYwnunZwSmi7Bal/u3m6IC5SZbjAAOjKBGZyfvHefK7SA==", + "version": "0.10.0-beta.107", + "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.107.tgz", + "integrity": "sha512-3qQZz6dPS8XlGGwz1p5Bqjoosah6Km1GwGeNv7YxkCVU2kpTOymFH8BMUEOFC5WHdYseDXnz8hIEleexF9Pa2g==", "license": "MIT", "dependencies": { "font-finder": "^1.1.0", @@ -556,64 +556,64 @@ "node": ">8.0.0" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.101" + "@xterm/xterm": "^5.6.0-beta.107" } }, "node_modules/@xterm/addon-progress": { - "version": "0.2.0-beta.7", - "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.2.0-beta.7.tgz", - "integrity": "sha512-1FrJcHm2R+s7auGTrb3rzTevFz5nTP8dLHmY24iVq1a3rPxrprCkfDkugQJsCNG0rd5GT1qk9YWjVcu3GO7gQw==", + "version": "0.2.0-beta.13", + "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.2.0-beta.13.tgz", + "integrity": "sha512-BLmX+JdA+LLOMbVLQm+gPqieeKaVMiugBFpCtQtBus3kCs/2/F8pU/XNGV31CnXxDdRjptyOGrPVRreF/rDdcg==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.101" + "@xterm/xterm": "^5.6.0-beta.107" } }, "node_modules/@xterm/addon-search": { - "version": "0.16.0-beta.101", - "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.101.tgz", - "integrity": "sha512-rrT9KQsQb/OUQwSVvAIKNFslEM2ux6824GZYPB6uYJbFkRwI+aGKiqs8UM264UcZotHylMSg3dYybGPBImTH3A==", + "version": "0.16.0-beta.107", + "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.107.tgz", + "integrity": "sha512-poHjeKjnTKtS6rHyu3r5lhW/0rdLcRMSJLDmT9xo9lfTuyhfcUl8gro44iPtscKY+LEtrhZcortTlElMewy/Bw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.101" + "@xterm/xterm": "^5.6.0-beta.107" } }, "node_modules/@xterm/addon-serialize": { - "version": "0.14.0-beta.101", - "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.101.tgz", - "integrity": "sha512-LOJtJroDjoHY9EhSAr0UuWZ59bnZFnZ73xvBT0AyEH0Oqd7MC0LZtI0oV4ifcQU90Eb1oDq3LRfgHm9vAtUrFg==", + "version": "0.14.0-beta.107", + "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.107.tgz", + "integrity": "sha512-MGaoO2zlNuSGofX5Xfbw+MU2wgAApEsDOPeYoEBSQyLf5BR7Z2rGVCxFyF5zNsIhNDov7ptQIVpCfonMVAgUvg==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.101" + "@xterm/xterm": "^5.6.0-beta.107" } }, "node_modules/@xterm/addon-unicode11": { - "version": "0.9.0-beta.101", - "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.101.tgz", - "integrity": "sha512-iu1Ry7im8NO3hITbYHbsxZKTxiJQSvg/tGR1EXK1lFIXe9gHc6bqTQPhvFYZ8xgPNt+V1AHZY9SpwcxgBOuxUA==", + "version": "0.9.0-beta.107", + "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.107.tgz", + "integrity": "sha512-XTfJ1G7u+8zOT6faCfEB49AbVFAX4XWUoiLtyXCfRvbrVuv5wv53T03dLtBtpwjWoFaveLKGl3ZiU7t3dpl0RA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.101" + "@xterm/xterm": "^5.6.0-beta.107" } }, "node_modules/@xterm/addon-webgl": { - "version": "0.19.0-beta.101", - "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.101.tgz", - "integrity": "sha512-g9YzOEqYS7MW1QirNRQhUsRJeFKxsksVQ6iT1dOScjZg7DRwil7/HNS03hQkgigW2Ku3hP5hK0WdXDe4np20gQ==", + "version": "0.19.0-beta.107", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.107.tgz", + "integrity": "sha512-wdUkYiV4PnR9+7+1giJoFm5xrguCOKTH4xdQ/6lYuSbgMS0y1ykpcQjDNshwHy0TyUuh4o4z1+rd48qNjqGM6Q==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.101" + "@xterm/xterm": "^5.6.0-beta.107" } }, "node_modules/@xterm/headless": { - "version": "5.6.0-beta.101", - "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-5.6.0-beta.101.tgz", - "integrity": "sha512-uAIo1b50keq3Ybps3Q5QcakVz570hY7gdU/71v52N2BxbvXy0wPk92Q4lCapsKmdtQ3+HbLtRsh1s4k0oP4VGw==", + "version": "5.6.0-beta.107", + "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-5.6.0-beta.107.tgz", + "integrity": "sha512-jFoJEPXjffcSzQyf/fTh3eZsXuHP/xQmkezkKbmrkzxw5h40NPbTPQbhTrsUCea2M12Mun1BrT8ScSYMjUCH3w==", "license": "MIT" }, "node_modules/@xterm/xterm": { - "version": "5.6.0-beta.101", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.101.tgz", - "integrity": "sha512-kV6Ad/KTcCgKWTYshufBEfT3OyadFLuskW+R+nJIJKrlAB34vRsX7TXFJ0P9QoMAeqXQpgngDfTn+RTAESyVyw==", + "version": "5.6.0-beta.107", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.107.tgz", + "integrity": "sha512-7cuJFZtc7Rv9BEpf9UsvErfXhLKkEmogI0mizgri+nNjEENnpQcFOpx84GFTjax/Zta8MAxOMUP1XU5Guju80A==", "license": "MIT" }, "node_modules/agent-base": { diff --git a/remote/package.json b/remote/package.json index e15e4dce8e2..63c54324b96 100644 --- a/remote/package.json +++ b/remote/package.json @@ -15,16 +15,16 @@ "@vscode/vscode-languagedetection": "1.0.21", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-clipboard": "^0.2.0-beta.84", - "@xterm/addon-image": "^0.9.0-beta.101", - "@xterm/addon-ligatures": "^0.10.0-beta.101", - "@xterm/addon-progress": "^0.2.0-beta.7", - "@xterm/addon-search": "^0.16.0-beta.101", - "@xterm/addon-serialize": "^0.14.0-beta.101", - "@xterm/addon-unicode11": "^0.9.0-beta.101", - "@xterm/addon-webgl": "^0.19.0-beta.101", - "@xterm/headless": "^5.6.0-beta.101", - "@xterm/xterm": "^5.6.0-beta.101", + "@xterm/addon-clipboard": "^0.2.0-beta.90", + "@xterm/addon-image": "^0.9.0-beta.107", + "@xterm/addon-ligatures": "^0.10.0-beta.107", + "@xterm/addon-progress": "^0.2.0-beta.13", + "@xterm/addon-search": "^0.16.0-beta.107", + "@xterm/addon-serialize": "^0.14.0-beta.107", + "@xterm/addon-unicode11": "^0.9.0-beta.107", + "@xterm/addon-webgl": "^0.19.0-beta.107", + "@xterm/headless": "^5.6.0-beta.107", + "@xterm/xterm": "^5.6.0-beta.107", "cookie": "^0.7.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", diff --git a/remote/web/package-lock.json b/remote/web/package-lock.json index a6c826c2a73..68dc9987bcf 100644 --- a/remote/web/package-lock.json +++ b/remote/web/package-lock.json @@ -13,15 +13,15 @@ "@vscode/iconv-lite-umd": "0.7.0", "@vscode/tree-sitter-wasm": "^0.1.4", "@vscode/vscode-languagedetection": "1.0.21", - "@xterm/addon-clipboard": "^0.2.0-beta.84", - "@xterm/addon-image": "^0.9.0-beta.101", - "@xterm/addon-ligatures": "^0.10.0-beta.101", - "@xterm/addon-progress": "^0.2.0-beta.7", - "@xterm/addon-search": "^0.16.0-beta.101", - "@xterm/addon-serialize": "^0.14.0-beta.101", - "@xterm/addon-unicode11": "^0.9.0-beta.101", - "@xterm/addon-webgl": "^0.19.0-beta.101", - "@xterm/xterm": "^5.6.0-beta.101", + "@xterm/addon-clipboard": "^0.2.0-beta.90", + "@xterm/addon-image": "^0.9.0-beta.107", + "@xterm/addon-ligatures": "^0.10.0-beta.107", + "@xterm/addon-progress": "^0.2.0-beta.13", + "@xterm/addon-search": "^0.16.0-beta.107", + "@xterm/addon-serialize": "^0.14.0-beta.107", + "@xterm/addon-unicode11": "^0.9.0-beta.107", + "@xterm/addon-webgl": "^0.19.0-beta.107", + "@xterm/xterm": "^5.6.0-beta.107", "jschardet": "3.1.4", "tas-client-umd": "0.2.0", "vscode-oniguruma": "1.7.0", @@ -90,30 +90,30 @@ } }, "node_modules/@xterm/addon-clipboard": { - "version": "0.2.0-beta.84", - "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.84.tgz", - "integrity": "sha512-/7lRpyLboTDKa1SMQCkLkUnH5hawiDsZ1VDMhfgjEr44ltw3cv2YuTtPQYkKen0vfu/0uzZeHWCwsZpQK25nRA==", + "version": "0.2.0-beta.90", + "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.90.tgz", + "integrity": "sha512-2i7qtACRBYRTRba831ufEjQMeAvK4uuVPYPBkSXzKJ/dIAvhws5B6OOmxqZzR97OsmzUC/SrjHSujgwyD0MVVw==", "license": "MIT", "dependencies": { "js-base64": "^3.7.5" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.101" + "@xterm/xterm": "^5.6.0-beta.107" } }, "node_modules/@xterm/addon-image": { - "version": "0.9.0-beta.101", - "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.101.tgz", - "integrity": "sha512-iAp4DFxqEhN1DWcCy3d66NgrAklKXfZhHlE8T0rvGS1mfK8ubO5WODXUdMO0rwU5TSrnt4l21DVwFhSs+2oWQw==", + "version": "0.9.0-beta.107", + "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.107.tgz", + "integrity": "sha512-/vqb2BhVjJeCcOIDSpjWrfXbhNTCse95qkKbpmzFkQJJEh0CyaqjSUBIAM+Qcl9f/x6H//O2wADnn3Wsn5qYsg==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.101" + "@xterm/xterm": "^5.6.0-beta.107" } }, "node_modules/@xterm/addon-ligatures": { - "version": "0.10.0-beta.101", - "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.101.tgz", - "integrity": "sha512-QgixJpyzP4ZFhv0YJJgNFXih7escNod9cGTAG7eW/dYwnunZwSmi7Bal/u3m6IC5SZbjAAOjKBGZyfvHefK7SA==", + "version": "0.10.0-beta.107", + "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.107.tgz", + "integrity": "sha512-3qQZz6dPS8XlGGwz1p5Bqjoosah6Km1GwGeNv7YxkCVU2kpTOymFH8BMUEOFC5WHdYseDXnz8hIEleexF9Pa2g==", "license": "MIT", "dependencies": { "font-finder": "^1.1.0", @@ -123,58 +123,58 @@ "node": ">8.0.0" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.101" + "@xterm/xterm": "^5.6.0-beta.107" } }, "node_modules/@xterm/addon-progress": { - "version": "0.2.0-beta.7", - "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.2.0-beta.7.tgz", - "integrity": "sha512-1FrJcHm2R+s7auGTrb3rzTevFz5nTP8dLHmY24iVq1a3rPxrprCkfDkugQJsCNG0rd5GT1qk9YWjVcu3GO7gQw==", + "version": "0.2.0-beta.13", + "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.2.0-beta.13.tgz", + "integrity": "sha512-BLmX+JdA+LLOMbVLQm+gPqieeKaVMiugBFpCtQtBus3kCs/2/F8pU/XNGV31CnXxDdRjptyOGrPVRreF/rDdcg==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.101" + "@xterm/xterm": "^5.6.0-beta.107" } }, "node_modules/@xterm/addon-search": { - "version": "0.16.0-beta.101", - "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.101.tgz", - "integrity": "sha512-rrT9KQsQb/OUQwSVvAIKNFslEM2ux6824GZYPB6uYJbFkRwI+aGKiqs8UM264UcZotHylMSg3dYybGPBImTH3A==", + "version": "0.16.0-beta.107", + "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.107.tgz", + "integrity": "sha512-poHjeKjnTKtS6rHyu3r5lhW/0rdLcRMSJLDmT9xo9lfTuyhfcUl8gro44iPtscKY+LEtrhZcortTlElMewy/Bw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.101" + "@xterm/xterm": "^5.6.0-beta.107" } }, "node_modules/@xterm/addon-serialize": { - "version": "0.14.0-beta.101", - "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.101.tgz", - "integrity": "sha512-LOJtJroDjoHY9EhSAr0UuWZ59bnZFnZ73xvBT0AyEH0Oqd7MC0LZtI0oV4ifcQU90Eb1oDq3LRfgHm9vAtUrFg==", + "version": "0.14.0-beta.107", + "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.107.tgz", + "integrity": "sha512-MGaoO2zlNuSGofX5Xfbw+MU2wgAApEsDOPeYoEBSQyLf5BR7Z2rGVCxFyF5zNsIhNDov7ptQIVpCfonMVAgUvg==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.101" + "@xterm/xterm": "^5.6.0-beta.107" } }, "node_modules/@xterm/addon-unicode11": { - "version": "0.9.0-beta.101", - "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.101.tgz", - "integrity": "sha512-iu1Ry7im8NO3hITbYHbsxZKTxiJQSvg/tGR1EXK1lFIXe9gHc6bqTQPhvFYZ8xgPNt+V1AHZY9SpwcxgBOuxUA==", + "version": "0.9.0-beta.107", + "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.107.tgz", + "integrity": "sha512-XTfJ1G7u+8zOT6faCfEB49AbVFAX4XWUoiLtyXCfRvbrVuv5wv53T03dLtBtpwjWoFaveLKGl3ZiU7t3dpl0RA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.101" + "@xterm/xterm": "^5.6.0-beta.107" } }, "node_modules/@xterm/addon-webgl": { - "version": "0.19.0-beta.101", - "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.101.tgz", - "integrity": "sha512-g9YzOEqYS7MW1QirNRQhUsRJeFKxsksVQ6iT1dOScjZg7DRwil7/HNS03hQkgigW2Ku3hP5hK0WdXDe4np20gQ==", + "version": "0.19.0-beta.107", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.107.tgz", + "integrity": "sha512-wdUkYiV4PnR9+7+1giJoFm5xrguCOKTH4xdQ/6lYuSbgMS0y1ykpcQjDNshwHy0TyUuh4o4z1+rd48qNjqGM6Q==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.101" + "@xterm/xterm": "^5.6.0-beta.107" } }, "node_modules/@xterm/xterm": { - "version": "5.6.0-beta.101", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.101.tgz", - "integrity": "sha512-kV6Ad/KTcCgKWTYshufBEfT3OyadFLuskW+R+nJIJKrlAB34vRsX7TXFJ0P9QoMAeqXQpgngDfTn+RTAESyVyw==", + "version": "5.6.0-beta.107", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.107.tgz", + "integrity": "sha512-7cuJFZtc7Rv9BEpf9UsvErfXhLKkEmogI0mizgri+nNjEENnpQcFOpx84GFTjax/Zta8MAxOMUP1XU5Guju80A==", "license": "MIT" }, "node_modules/font-finder": { diff --git a/remote/web/package.json b/remote/web/package.json index 167d8e4dbba..1f3b42ac717 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -8,15 +8,15 @@ "@vscode/iconv-lite-umd": "0.7.0", "@vscode/tree-sitter-wasm": "^0.1.4", "@vscode/vscode-languagedetection": "1.0.21", - "@xterm/addon-clipboard": "^0.2.0-beta.84", - "@xterm/addon-image": "^0.9.0-beta.101", - "@xterm/addon-ligatures": "^0.10.0-beta.101", - "@xterm/addon-progress": "^0.2.0-beta.7", - "@xterm/addon-search": "^0.16.0-beta.101", - "@xterm/addon-serialize": "^0.14.0-beta.101", - "@xterm/addon-unicode11": "^0.9.0-beta.101", - "@xterm/addon-webgl": "^0.19.0-beta.101", - "@xterm/xterm": "^5.6.0-beta.101", + "@xterm/addon-clipboard": "^0.2.0-beta.90", + "@xterm/addon-image": "^0.9.0-beta.107", + "@xterm/addon-ligatures": "^0.10.0-beta.107", + "@xterm/addon-progress": "^0.2.0-beta.13", + "@xterm/addon-search": "^0.16.0-beta.107", + "@xterm/addon-serialize": "^0.14.0-beta.107", + "@xterm/addon-unicode11": "^0.9.0-beta.107", + "@xterm/addon-webgl": "^0.19.0-beta.107", + "@xterm/xterm": "^5.6.0-beta.107", "jschardet": "3.1.4", "tas-client-umd": "0.2.0", "vscode-oniguruma": "1.7.0", diff --git a/src/main.ts b/src/main.ts index 1af3c941e00..fc3b6ad6a35 100644 --- a/src/main.ts +++ b/src/main.ts @@ -249,7 +249,10 @@ function configureCommandlineSwitchesSync(cliArgs: NativeParsedArgs) { 'log-level', // Use an in-memory storage for secrets - 'use-inmemory-secretstorage' + 'use-inmemory-secretstorage', + + // Enables display tracking to restore maximized windows under RDP: https://github.com/electron/electron/issues/47016 + 'enable-rdp-display-tracking' ]; // Read argv config @@ -307,6 +310,12 @@ function configureCommandlineSwitchesSync(cliArgs: NativeParsedArgs) { process.argv.push('--use-inmemory-secretstorage'); } break; + + case 'enable-rdp-display-tracking': + if (argvValue) { + process.argv.push('--enable-rdp-display-tracking'); + } + break; } } }); @@ -359,6 +368,7 @@ interface IArgvConfig { readonly 'log-level'?: string | string[]; readonly 'disable-chromium-sandbox'?: boolean; readonly 'use-inmemory-secretstorage'?: boolean; + readonly 'enable-rdp-display-tracking'?: boolean; } function readArgvConfigSync(): IArgvConfig { diff --git a/src/tsconfig.defineClassFields.json b/src/tsconfig.defineClassFields.json new file mode 100644 index 00000000000..1a4a16ebaf3 --- /dev/null +++ b/src/tsconfig.defineClassFields.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "useDefineForClassFields": true, + "noEmit": true, + "skipLibCheck": true + } +} diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index 7227aba24f3..553c584a63d 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -1544,7 +1544,7 @@ export function triggerDownload(dataOrUri: Uint8Array | URI, name: string): void if (URI.isUri(dataOrUri)) { url = dataOrUri.toString(true); } else { - const blob = new Blob([dataOrUri]); + const blob = new Blob([dataOrUri as Uint8Array]); url = URL.createObjectURL(blob); // Ensure to free the data from DOM eventually diff --git a/src/vs/base/browser/domImpl/n.ts b/src/vs/base/browser/domImpl/n.ts index cab200cc0e8..a3c7653a51c 100644 --- a/src/vs/base/browser/domImpl/n.ts +++ b/src/vs/base/browser/domImpl/n.ts @@ -5,7 +5,7 @@ import { BugIndicatingError } from '../../common/errors.js'; import { DisposableStore, IDisposable } from '../../common/lifecycle.js'; -import { derived, derivedOpts, derivedWithStore, IObservable, IReader, observableValue } from '../../common/observable.js'; +import { derived, derivedOpts, IObservable, IReader, observableValue } from '../../common/observable.js'; import { isSVGElement } from '../dom.js'; export namespace n { @@ -127,9 +127,9 @@ export abstract class ObserverNode { ref(this._element); } if (obsRef) { - this._deriveds.push(derivedWithStore((_reader, store) => { + this._deriveds.push(derived((_reader) => { obsRef(this as unknown as ObserverNodeWithElement); - store.add({ + _reader.store.add({ dispose: () => { obsRef(null); } @@ -364,8 +364,8 @@ function camelCaseToHyphenCase(str: string) { return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); } -function isObservable(obj: any): obj is IObservable { - return obj && typeof obj === 'object' && obj['read'] !== undefined && obj['reportChanges'] !== undefined; +function isObservable(obj: unknown): obj is IObservable { + return !!obj && (>obj).read !== undefined && (>obj).reportChanges !== undefined; } type ElementAttributeKeys = Partial<{ diff --git a/src/vs/base/browser/markdownRenderer.ts b/src/vs/base/browser/markdownRenderer.ts index bf2739b3d43..2cb748afcdf 100644 --- a/src/vs/base/browser/markdownRenderer.ts +++ b/src/vs/base/browser/markdownRenderer.ts @@ -26,8 +26,8 @@ import { StandardKeyboardEvent } from './keyboardEvent.js'; import { StandardMouseEvent } from './mouseEvent.js'; import { renderLabelWithIcons } from './ui/iconLabel/iconLabels.js'; -export interface MarkedOptions extends marked.MarkedOptions { - baseUrl?: never; +export interface MarkedOptions extends Readonly> { + readonly markedExtensions?: marked.MarkedExtension[]; } export interface MarkdownRenderOptions extends FormattedTextRenderOptions { @@ -99,13 +99,14 @@ const defaultMarkedRenderers = Object.freeze({ * **Note** that for most cases you should be using {@link import('../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js').MarkdownRenderer MarkdownRenderer} * which comes with support for pretty code block rendering and which uses the default way of handling links. */ -export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRenderOptions = {}, markedOptions: Readonly = {}): { element: HTMLElement; dispose: () => void } { +export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRenderOptions = {}, markedOptions: MarkedOptions = {}): { element: HTMLElement; dispose: () => void } { const disposables = new DisposableStore(); let isDisposed = false; const element = createElement(options); - const { renderer, codeBlocks, syncCodeBlocks } = createMarkdownRenderer(options, markdown); + const markedInstance = new marked.Marked(...(markedOptions.markedExtensions ?? [])); + const { renderer, codeBlocks, syncCodeBlocks } = createMarkdownRenderer(markedInstance, options, markdown); const value = preprocessMarkdownString(markdown); let renderedMarkdown: string; @@ -116,11 +117,11 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende ...markedOptions, renderer }; - const tokens = marked.lexer(value, opts); + const tokens = markedInstance.lexer(value, opts); const newTokens = fillInIncompleteTokens(tokens); - renderedMarkdown = marked.parser(newTokens, opts); + renderedMarkdown = markedInstance.parser(newTokens, opts); } else { - renderedMarkdown = marked.parse(value, { ...markedOptions, renderer, async: false }); + renderedMarkdown = markedInstance.parse(value, { ...markedOptions, renderer, async: false }); } // Rewrite theme icons @@ -243,7 +244,7 @@ function rewriteRenderedLinks(markdown: IMarkdownString, options: MarkdownRender } } -function createMarkdownRenderer(options: MarkdownRenderOptions, markdown: IMarkdownString): { renderer: marked.Renderer; codeBlocks: Promise<[string, HTMLElement]>[]; syncCodeBlocks: [string, HTMLElement][] } { +function createMarkdownRenderer(marked: marked.Marked, options: MarkdownRenderOptions, markdown: IMarkdownString): { renderer: marked.Renderer; codeBlocks: Promise<[string, HTMLElement]>[]; syncCodeBlocks: [string, HTMLElement][] } { const renderer = new marked.Renderer(); renderer.image = defaultMarkedRenderers.image; renderer.link = defaultMarkedRenderers.link; @@ -527,6 +528,7 @@ function getSanitizerOptions(options: IInternalSanitizerOptions): { config: domp Schemas.vscodeFileResource, Schemas.vscodeRemote, Schemas.vscodeRemoteResource, + Schemas.vscodeNotebookCell ]; if (options.isTrusted) { diff --git a/src/vs/base/browser/ui/button/button.ts b/src/vs/base/browser/ui/button/button.ts index d276da47dc7..36f046f1833 100644 --- a/src/vs/base/browser/ui/button/button.ts +++ b/src/vs/base/browser/ui/button/button.ts @@ -464,6 +464,7 @@ export class ButtonWithDropdown extends Disposable implements IButton { } export class ButtonWithDescription implements IButtonWithDescription { + private _button: Button; private _element: HTMLElement; private _descriptionElement: HTMLElement; @@ -531,14 +532,17 @@ export class ButtonWithDescription implements IButtonWithDescription { } } +export enum ButtonBarAlignment { + Horizontal = 0, + Vertical +} + export class ButtonBar { private readonly _buttons: IButton[] = []; private readonly _buttonStore = new DisposableStore(); - constructor(private readonly container: HTMLElement) { - - } + constructor(private readonly container: HTMLElement, private readonly options?: { alignment?: ButtonBarAlignment }) { } dispose(): void { this._buttonStore.dispose(); @@ -581,9 +585,9 @@ export class ButtonBar { // Next / Previous Button let buttonIndexToFocus: number | undefined; - if (event.equals(KeyCode.LeftArrow)) { + if (event.equals(this.options?.alignment === ButtonBarAlignment.Vertical ? KeyCode.UpArrow : KeyCode.LeftArrow)) { buttonIndexToFocus = index > 0 ? index - 1 : this._buttons.length - 1; - } else if (event.equals(KeyCode.RightArrow)) { + } else if (event.equals(this.options?.alignment === ButtonBarAlignment.Vertical ? KeyCode.DownArrow : KeyCode.RightArrow)) { buttonIndexToFocus = index === this._buttons.length - 1 ? 0 : index + 1; } else { eventHandled = false; diff --git a/src/vs/base/browser/ui/dialog/dialog.css b/src/vs/base/browser/ui/dialog/dialog.css index c7d4da10488..c761d2f2434 100644 --- a/src/vs/base/browser/ui/dialog/dialog.css +++ b/src/vs/base/browser/ui/dialog/dialog.css @@ -33,6 +33,10 @@ border-radius: 3px; } +.monaco-dialog-box.align-vertical { + min-width: 350px; /* more narrow when aligned vertically */ +} + /** Dialog: Title Actions Row */ .monaco-dialog-box .dialog-toolbar-row { height: 22px; @@ -43,35 +47,59 @@ justify-content: flex-end; } -/** Dialog: Message Row */ -.monaco-dialog-box .dialog-message-row { +/** Dialog: Message/Footer Row */ +.monaco-dialog-box .dialog-message-row, +.monaco-dialog-box .dialog-footer-row { display: flex; flex-grow: 1; align-items: center; padding: 0 10px; } +.monaco-dialog-box.align-vertical .dialog-message-row { + flex-direction: column; +} + .monaco-dialog-box .dialog-message-row > .dialog-icon.codicon { flex: 0 0 48px; height: 48px; - align-self: baseline; font-size: 48px; } -/** Dialog: Message Container */ -.monaco-dialog-box .dialog-message-row .dialog-message-container { +.monaco-dialog-box:not(.align-vertical) .dialog-message-row > .dialog-icon.codicon { + align-self: baseline; +} + +/** Dialog: Message/Footer Container */ +.monaco-dialog-box .dialog-message-row .dialog-message-container, +.monaco-dialog-box .dialog-footer-row { display: flex; flex-direction: column; overflow: hidden; text-overflow: ellipsis; - padding-left: 24px; user-select: text; -webkit-user-select: text; word-wrap: break-word; /* never overflow long words, but break to next line */ white-space: normal; } -.monaco-dialog-box .dialog-message-row .dialog-message-container ul { +.monaco-dialog-box .dialog-footer-row { + margin-top: 20px; +} + +.monaco-dialog-box:not(.align-vertical) .dialog-message-row .dialog-message-container, +.monaco-dialog-box:not(.align-vertical) .dialog-footer-row { + padding-left: 24px; +} + +.monaco-dialog-box.align-vertical .dialog-message-row .dialog-message-container, +.monaco-dialog-box.align-vertical .dialog-footer-row { + align-items: center; + text-align: center; +} + +.monaco-dialog-box .dialog-message-row .dialog-message-container ul, +.monaco-dialog-box .dialog-footer-row ul { padding-inline-start: 20px; /* reduce excessive indent of list items in the dialog */ } @@ -144,13 +172,21 @@ .monaco-dialog-box > .dialog-buttons-row > .dialog-buttons { display: flex; width: 100%; - justify-content: flex-end; +} + +.monaco-dialog-box:not(.align-vertical) > .dialog-buttons-row > .dialog-buttons { overflow: hidden; + justify-content: flex-end; margin-left: 67px; /* for long buttons, force align with text */ } +.monaco-dialog-box.align-vertical > .dialog-buttons-row > .dialog-buttons { + margin-left: 5px; + margin-right: 5px; + flex-direction: column; +} + .monaco-dialog-box > .dialog-buttons-row > .dialog-buttons > .monaco-button { - width: fit-content; padding: 5px 10px; overflow: hidden; text-overflow: ellipsis; @@ -158,11 +194,34 @@ outline-offset: 2px !important; } +.monaco-dialog-box.align-vertical > .dialog-buttons-row > .dialog-buttons > .monaco-button { + margin: 4px 0; /* allows button focus outline to be visible */ +} + +.monaco-dialog-box:not(.align-vertical) > .dialog-buttons-row > .dialog-buttons > .monaco-button { + width: fit-content; +} + +.monaco-dialog-box > .dialog-buttons-row > .dialog-buttons > .monaco-button.link-button { + border: 0 !important; + background-color: unset !important; + color: var(--vscode-textLink-foreground) !important; +} + +.monaco-dialog-box > .dialog-buttons-row > .dialog-buttons > .monaco-button.link-button:active, +.monaco-dialog-box > .dialog-buttons-row > .dialog-buttons > .monaco-button.link-button:hover { + color: var(--vscode-textLink-activeForeground) !important; +} + /** Dialog: Dropdown */ -.monaco-dialog-box > .dialog-buttons-row > .dialog-buttons > .monaco-button-dropdown { +.monaco-dialog-box:not(.align-vertical) > .dialog-buttons-row > .dialog-buttons > .monaco-button-dropdown { margin: 4px 5px; } +.monaco-dialog-box.align-vertical > .dialog-buttons-row > .dialog-buttons > .monaco-button-dropdown { + width: 100%; +} + .monaco-dialog-box > .dialog-buttons-row > .dialog-buttons > .monaco-button-dropdown:focus-within { /** * This is a trick to make the focus outline appear on the entire @@ -181,6 +240,10 @@ padding-right: 10px; } +.monaco-dialog-box.align-vertical > .dialog-buttons-row > .dialog-buttons > .monaco-button-dropdown > .monaco-text-button { + width: 100%; +} + .monaco-dialog-box > .dialog-buttons-row > .dialog-buttons > .monaco-button-dropdown > .monaco-dropdown-button { padding-left: 5px; padding-right: 5px; diff --git a/src/vs/base/browser/ui/dialog/dialog.ts b/src/vs/base/browser/ui/dialog/dialog.ts index 6e68f0f8e68..280f0eea750 100644 --- a/src/vs/base/browser/ui/dialog/dialog.ts +++ b/src/vs/base/browser/ui/dialog/dialog.ts @@ -8,7 +8,7 @@ import { localize } from '../../../../nls.js'; import { $, addDisposableListener, clearNode, EventHelper, EventType, getWindow, hide, isActiveElement, isAncestor, show } from '../../dom.js'; import { StandardKeyboardEvent } from '../../keyboardEvent.js'; import { ActionBar } from '../actionbar/actionbar.js'; -import { ButtonBar, ButtonWithDescription, ButtonWithDropdown, IButton, IButtonStyles, IButtonWithDropdownOptions } from '../button/button.js'; +import { ButtonBar, ButtonBarAlignment, ButtonWithDescription, ButtonWithDropdown, IButton, IButtonStyles, IButtonWithDropdownOptions } from '../button/button.js'; import { ICheckboxStyles, Checkbox } from '../toggle/toggle.js'; import { IInputBoxStyles, InputBox } from '../inputbox/inputBox.js'; import { Action, toAction } from '../../../common/actions.js'; @@ -26,17 +26,33 @@ export interface IDialogInputOptions { readonly value?: string; } +export enum DialogContentsAlignment { + /** + * Dialog contents align from left to right (icon, message, buttons on a separate row). + * + * Note: this is the default alignment for dialogs. + */ + Horizontal = 0, + + /** + * Dialog contents align from top to bottom (icon, message, buttons stack on top of each other) + */ + Vertical +} + export interface IDialogOptions { readonly cancelId?: number; readonly detail?: string; + readonly alignment?: DialogContentsAlignment; readonly checkboxLabel?: string; readonly checkboxChecked?: boolean; readonly type?: 'none' | 'info' | 'error' | 'question' | 'warning' | 'pending'; readonly inputs?: IDialogInputOptions[]; readonly keyEventProcessor?: (event: StandardKeyboardEvent) => void; readonly renderBody?: (container: HTMLElement) => void; + readonly renderFooter?: (container: HTMLElement) => void; readonly icon?: ThemeIcon; - readonly buttonDetails?: string[]; + readonly buttonOptions?: Array; readonly primaryButtonDropdown?: IButtonWithDropdownOptions; readonly disableCloseAction?: boolean; readonly disableDefaultAction?: boolean; @@ -77,6 +93,7 @@ export class Dialog extends Disposable { private readonly buttonsContainer: HTMLElement; private readonly messageDetailElement: HTMLElement; private readonly messageContainer: HTMLElement; + private readonly footerContainer: HTMLElement | undefined; private readonly iconElement: HTMLElement; private readonly checkbox: Checkbox | undefined; private readonly toolbarContainer: HTMLElement; @@ -92,10 +109,26 @@ export class Dialog extends Disposable { this.modalElement = this.container.appendChild($(`.monaco-dialog-modal-block.dimmed`)); this.shadowElement = this.modalElement.appendChild($('.dialog-shadow')); this.element = this.shadowElement.appendChild($('.monaco-dialog-box')); + if (options.alignment === DialogContentsAlignment.Vertical) { + this.element.classList.add('align-vertical'); + } this.element.setAttribute('role', 'dialog'); this.element.tabIndex = -1; hide(this.element); + // Footer + if (this.options.renderFooter) { + this.footerContainer = this.element.appendChild($('.dialog-footer-row')); + + const customFooter = this.footerContainer.appendChild($('#monaco-dialog-footer.dialog-footer')); + this.options.renderFooter(customFooter); + + for (const el of this.footerContainer.querySelectorAll('a')) { + el.tabIndex = 0; + } + } + + // Buttons this.buttonStyles = options.buttonStyles; if (Array.isArray(buttons) && buttons.length > 0) { @@ -108,20 +141,19 @@ export class Dialog extends Disposable { const buttonsRowElement = this.element.appendChild($('.dialog-buttons-row')); this.buttonsContainer = buttonsRowElement.appendChild($('.dialog-buttons')); + // Message const messageRowElement = this.element.appendChild($('.dialog-message-row')); this.iconElement = messageRowElement.appendChild($('#monaco-dialog-icon.dialog-icon')); this.iconElement.setAttribute('aria-label', this.getIconAriaLabel()); this.messageContainer = messageRowElement.appendChild($('.dialog-message-container')); - if (this.options.detail || this.options.renderBody) { - const messageElement = this.messageContainer.appendChild($('.dialog-message')); - const messageTextElement = messageElement.appendChild($('#monaco-dialog-message-text.dialog-message-text')); - messageTextElement.innerText = this.message; - } + const messageElement = this.messageContainer.appendChild($('.dialog-message')); + const messageTextElement = messageElement.appendChild($('#monaco-dialog-message-text.dialog-message-text')); + messageTextElement.innerText = this.message; this.messageDetailElement = this.messageContainer.appendChild($('#monaco-dialog-message-detail.dialog-message-detail')); - if (this.options.detail || !this.options.renderBody) { - this.messageDetailElement.innerText = this.options.detail ? this.options.detail : message; + if (this.options.detail) { + this.messageDetailElement.innerText = this.options.detail; } else { this.messageDetailElement.style.display = 'none'; } @@ -135,6 +167,7 @@ export class Dialog extends Disposable { } } + // Inputs if (this.options.inputs) { this.inputs = this.options.inputs.map(input => { const inputRowElement = this.messageContainer.appendChild($('.dialog-message-input')); @@ -155,6 +188,7 @@ export class Dialog extends Disposable { this.inputs = []; } + // Checkbox if (this.options.checkboxLabel) { const checkboxRowElement = this.messageContainer.appendChild($('.dialog-checkbox-row')); @@ -169,6 +203,7 @@ export class Dialog extends Disposable { this._register(addDisposableListener(checkboxMessageElement, EventType.CLICK, () => checkbox.checked = !checkbox.checked)); } + // Toolbar const toolbarRowElement = this.element.appendChild($('.dialog-toolbar-row')); this.toolbarContainer = toolbarRowElement.appendChild($('.dialog-toolbar')); @@ -216,7 +251,7 @@ export class Dialog extends Disposable { }; this._register(toDisposable(close)); - const buttonBar = this.buttonBar = this._register(new ButtonBar(this.buttonsContainer)); + const buttonBar = this.buttonBar = this._register(new ButtonBar(this.buttonsContainer, { alignment: this.options?.alignment === DialogContentsAlignment.Vertical ? ButtonBarAlignment.Vertical : ButtonBarAlignment.Horizontal })); const buttonMap = this.rearrangeButtons(this.buttons, this.options.cancelId); const onButtonClick = (index: number) => { @@ -232,7 +267,11 @@ export class Dialog extends Disposable { const primary = buttonMap[index].index === 0; let button: IButton; - if (primary && this.options?.primaryButtonDropdown) { + const buttonOptions = this.options.buttonOptions?.[buttonMap[index]?.index]; + if (buttonOptions?.renderAsLink) { + button = this._register(buttonBar.addButton({ secondary: !primary, ...this.buttonStyles })); + button.element.classList.add('link-button'); + } else if (primary && this.options?.primaryButtonDropdown) { const actions = isActionProvider(this.options.primaryButtonDropdown.actions) ? this.options.primaryButtonDropdown.actions.getActions() : this.options.primaryButtonDropdown.actions; button = this._register(buttonBar.addButtonWithDropdown({ ...this.options.primaryButtonDropdown, @@ -247,7 +286,7 @@ export class Dialog extends Disposable { } })) })); - } else if (this.options.buttonDetails) { + } else if (buttonOptions?.sublabel) { button = this._register(buttonBar.addButtonWithDescription({ secondary: !primary, ...this.buttonStyles })); } else { button = this._register(buttonBar.addButton({ secondary: !primary, ...this.buttonStyles })); @@ -255,7 +294,9 @@ export class Dialog extends Disposable { button.label = mnemonicButtonLabel(buttonMap[index].label, true); if (button instanceof ButtonWithDescription) { - button.description = this.options.buttonDetails![buttonMap[index].index]; + if (buttonOptions?.sublabel) { + button.description = buttonOptions?.sublabel; + } } this._register(button.onDidClick(e => { if (e) { @@ -364,6 +405,16 @@ export class Dialog extends Disposable { } } + if (this.footerContainer) { + const links = this.footerContainer.querySelectorAll('a'); + for (const link of links) { + focusableElements.push(link); + if (isActiveElement(link)) { + focusedIndex = focusableElements.length - 1; + } + } + } + // Focus next element (with wrapping) if (evt.equals(KeyCode.Tab) || evt.equals(KeyCode.RightArrow)) { const newFocusedIndex = (focusedIndex + 1) % focusableElements.length; @@ -463,7 +514,7 @@ export class Dialog extends Disposable { this.element.setAttribute('aria-modal', 'true'); this.element.setAttribute('aria-labelledby', 'monaco-dialog-icon monaco-dialog-message-text'); - this.element.setAttribute('aria-describedby', 'monaco-dialog-icon monaco-dialog-message-text monaco-dialog-message-detail monaco-dialog-message-body'); + this.element.setAttribute('aria-describedby', 'monaco-dialog-icon monaco-dialog-message-text monaco-dialog-message-detail monaco-dialog-message-body monaco-dialog-footer'); show(this.element); // Focus first element (input or button) @@ -495,14 +546,8 @@ export class Dialog extends Disposable { this.element.style.backgroundColor = bgColor ?? ''; this.element.style.border = border; - // TODO fix - // if (fgColor && bgColor) { - // const messageDetailColor = fgColor.transparent(.9); - // this.messageDetailElement.style.mixBlendMode = messageDetailColor.makeOpaque(bgColor).toString(); - // } - if (linkFgColor) { - for (const el of this.messageContainer.getElementsByTagName('a')) { + for (const el of [...this.messageContainer.getElementsByTagName('a'), ...this.footerContainer?.getElementsByTagName('a') ?? []]) { el.style.color = linkFgColor; } } @@ -546,8 +591,8 @@ export class Dialog extends Disposable { // so that when we move them around it's not a problem const buttonMap: ButtonMapEntry[] = buttons.map((label, index) => ({ label, index })); - if (buttons.length < 2) { - return buttonMap; // only need to rearrange if there are 2+ buttons + if (buttons.length < 2 || this.options.alignment === DialogContentsAlignment.Vertical) { + return buttonMap; // only need to rearrange if there are 2+ buttons and the alignment is left-to-right } if (isMacintosh || isLinux) { diff --git a/src/vs/base/browser/ui/list/listView.ts b/src/vs/base/browser/ui/list/listView.ts index 2db1250ff86..4fb87aeb6f2 100644 --- a/src/vs/base/browser/ui/list/listView.ts +++ b/src/vs/base/browser/ui/list/listView.ts @@ -194,8 +194,8 @@ function equalsDragFeedback(f1: number[] | undefined, f2: number[] | undefined): class ListViewAccessibilityProvider implements Required> { - readonly getSetSize: (element: any, index: number, listLength: number) => number; - readonly getPosInSet: (element: any, index: number) => number; + readonly getSetSize: (element: T, index: number, listLength: number) => number; + readonly getPosInSet: (element: T, index: number) => number; readonly getRole: (element: T) => AriaRole | undefined; readonly isChecked: (element: T) => boolean | IValueWithChangeEvent | undefined; diff --git a/src/vs/base/browser/ui/list/listWidget.ts b/src/vs/base/browser/ui/list/listWidget.ts index 44b59334606..47a4bb791db 100644 --- a/src/vs/base/browser/ui/list/listWidget.ts +++ b/src/vs/base/browser/ui/list/listWidget.ts @@ -1259,7 +1259,7 @@ class PipelineRenderer implements IListRenderer { } } - disposeTemplate(templateData: any[]): void { + disposeTemplate(templateData: unknown[]): void { let i = 0; for (const renderer of this.renderers) { @@ -1307,7 +1307,7 @@ class AccessibiltyRenderer implements IListRenderer extends ITreeNode(obj: any): obj is ITreeFilterDataResult { - return typeof obj === 'object' && 'visibility' in obj && 'data' in obj; +export function isFilterResult(obj: unknown): obj is ITreeFilterDataResult { + return !!obj && (>obj).visibility !== undefined; } export function getVisibleState(visibility: boolean | TreeVisibility): TreeVisibility { diff --git a/src/vs/base/browser/webWorkerFactory.ts b/src/vs/base/browser/webWorkerFactory.ts index 0cd7255e525..58a43681228 100644 --- a/src/vs/base/browser/webWorkerFactory.ts +++ b/src/vs/base/browser/webWorkerFactory.ts @@ -108,11 +108,8 @@ function whenESMWorkerReady(worker: Worker): Promise { }); } -function isPromiseLike(obj: any): obj is PromiseLike { - if (typeof obj.then === 'function') { - return true; - } - return false; +function isPromiseLike(obj: unknown): obj is PromiseLike { + return !!obj && typeof (obj as PromiseLike).then === 'function'; } /** @@ -175,7 +172,7 @@ class WebWorker extends Disposable implements IWebWorker { return this.id; } - public postMessage(message: any, transfer: Transferable[]): void { + public postMessage(message: unknown, transfer: Transferable[]): void { this.worker?.then(w => { try { w.postMessage(message, transfer); diff --git a/src/vs/base/common/arrays.ts b/src/vs/base/common/arrays.ts index ff00c68b040..3ee00d10979 100644 --- a/src/vs/base/common/arrays.ts +++ b/src/vs/base/common/arrays.ts @@ -372,7 +372,7 @@ export function move(array: unknown[], from: number, to: number): void { /** * @returns false if the provided object is an array and not empty. */ -export function isFalsyOrEmpty(obj: any): boolean { +export function isFalsyOrEmpty(obj: unknown): boolean { return !Array.isArray(obj) || obj.length === 0; } @@ -625,26 +625,7 @@ function getActualStartIndex(array: T[], start: number): number { } /** - * Utility that helps to pick a property from an object. - * - * ## Examples - * - * ```typescript - * interface IObject = { - * a: number, - * b: string, - * }; - * - * const list: IObject[] = [ - * { a: 1, b: 'foo' }, - * { a: 2, b: 'bar' }, - * ]; - * - * assert.deepStrictEqual( - * list.map(pick('a')), - * [1, 2], - * ); - * ``` + * @deprecated do not use, use property access */ export const pick = ( key: TKeyName, diff --git a/src/vs/base/common/async.ts b/src/vs/base/common/async.ts index a53c2c490c9..4f4b650cdf8 100644 --- a/src/vs/base/common/async.ts +++ b/src/vs/base/common/async.ts @@ -68,10 +68,10 @@ export function createCancelablePromise(callback: (token: CancellationToken) source.cancel(); source.dispose(); } - then(resolve?: ((value: T) => TResult1 | Promise) | undefined | null, reject?: ((reason: any) => TResult2 | Promise) | undefined | null): Promise { + then(resolve?: ((value: T) => TResult1 | Promise) | undefined | null, reject?: ((reason: unknown) => TResult2 | Promise) | undefined | null): Promise { return promise.then(resolve, reject); } - catch(reject?: ((reason: any) => TResult | Promise) | undefined | null): Promise { + catch(reject?: ((reason: unknown) => TResult | Promise) | undefined | null): Promise { return this.then(undefined, reject); } finally(onfinally?: (() => void) | undefined | null): Promise { @@ -388,7 +388,7 @@ export class Delayer implements IDisposable { private deferred: IScheduledLater | null; private completionPromise: Promise | null; private doResolve: ((value?: any | Promise) => void) | null; - private doReject: ((err: any) => void) | null; + private doReject: ((err: unknown) => void) | null; private task: ITask> | null; constructor(public defaultDelay: number | typeof MicrotaskDelay) { @@ -603,9 +603,9 @@ export function sequence(promiseFactories: ITask>[]): Promise return index < len ? promiseFactories[index++]() : null; } - function thenHandler(result: any): Promise { + function thenHandler(result: unknown): Promise { if (result !== undefined && result !== null) { - results.push(result); + results.push(result as T); } const n = next(); diff --git a/src/vs/base/common/codecs/asyncDecoder.ts b/src/vs/base/common/codecs/asyncDecoder.ts index e35daa77317..28cbe686e47 100644 --- a/src/vs/base/common/codecs/asyncDecoder.ts +++ b/src/vs/base/common/codecs/asyncDecoder.ts @@ -7,7 +7,7 @@ import { Disposable } from '../lifecycle.js'; import { BaseDecoder } from './baseDecoder.js'; /** - * Asynchronous interator wrapper for a decoder. + * Asynchronous iterator wrapper for a decoder. */ export class AsyncDecoder, K extends NonNullable = NonNullable> extends Disposable { // Buffer of messages that have been decoded but not yet consumed. @@ -16,7 +16,7 @@ export class AsyncDecoder, K extends NonNullable< /** * A transient promise that is resolved when a new event * is received. Used in the situation when there is no new - * data avaialble and decoder stream did not finish yet, + * data available and decoder stream did not finish yet, * hence we need to wait until new event is received. */ private resolveOnNewEvent?: (value: void) => void; @@ -24,7 +24,7 @@ export class AsyncDecoder, K extends NonNullable< /** * @param decoder The decoder instance to wrap. * - * Note! Assumes ownership of the `decoder` object, hence will `dipose` + * Note! Assumes ownership of the `decoder` object, hence will `dispose` * it when the decoder stream is ended. */ constructor( diff --git a/src/vs/base/common/codecs/baseDecoder.ts b/src/vs/base/common/codecs/baseDecoder.ts index 7d590868eea..bdb649b105e 100644 --- a/src/vs/base/common/codecs/baseDecoder.ts +++ b/src/vs/base/common/codecs/baseDecoder.ts @@ -3,11 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { assert, assertNever } from '../assert.js'; import { Emitter } from '../event.js'; import { ReadableStream } from '../stream.js'; import { DeferredPromise } from '../async.js'; import { AsyncDecoder } from './asyncDecoder.js'; +import { assert, assertNever } from '../assert.js'; import { DisposableMap, IDisposable } from '../lifecycle.js'; import { ObservableDisposable } from '../observableDisposable.js'; @@ -105,11 +105,11 @@ export abstract class BaseDecoder< */ public start(): this { assert( - !this._ended, + this._ended === false, 'Cannot start stream that has already ended.', ); assert( - !this.disposed, + this.isDisposed === false, 'Cannot start stream that has already disposed.', ); @@ -119,9 +119,15 @@ export abstract class BaseDecoder< } this.started = true; - this.stream.on('data', this.tryOnStreamData); - this.stream.on('error', this.onStreamError); + /** + * !NOTE! The order of event subscriptions is critical here because + * the `data` event is also starts the stream, hence changing + * the order of event subscriptions can lead to race conditions. + * See {@link ReadableStreamEvents} for more info. + */ this.stream.on('end', this.onStreamEnd); + this.stream.on('error', this.onStreamError); + this.stream.on('data', this.tryOnStreamData); // this allows to compose decoders together, - if a decoder // instance is passed as a readable stream to this decoder, @@ -244,7 +250,7 @@ export abstract class BaseDecoder< */ public resume(): void { assert( - !this.ended, + this.ended === false, 'Cannot resume the stream because it has already ended.', ); diff --git a/src/vs/base/common/codecs/types/ICodec.d.ts b/src/vs/base/common/codecs/types/ICodec.d.ts deleted file mode 100644 index cd8abb3ff67..00000000000 --- a/src/vs/base/common/codecs/types/ICodec.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { ReadableStream } from '../../stream.js'; - -/** - * A codec is an object capable of encoding/decoding a stream of data transforming its messages. - * Useful for abstracting a data transfer or protocol logic on top of a stream of bytes. - * - * For instance, if protocol messages need to be trasferred over `TCP` connection, a codec that - * encodes the messages into a sequence of bytes before sending it to a network socket. Likewise, - * on the other end of the connection, the same codec can decode the sequence of bytes back into - * a sequence of the protocol messages. - */ -export interface ICodec { - /** - * Encode a stream of `K`s into a stream of `T`s. - */ - encode: (value: ReadableStream) => ReadableStream; - - /** - * Decode a stream of `T`s into a stream of `K`s. - */ - decode: (value: ReadableStream) => ReadableStream; -} diff --git a/src/vs/base/common/console.ts b/src/vs/base/common/console.ts index fded1d1c17e..f4790644524 100644 --- a/src/vs/base/common/console.ts +++ b/src/vs/base/common/console.ts @@ -21,7 +21,7 @@ export interface IStackFrame { column: number; } -export function isRemoteConsoleLog(obj: any): obj is IRemoteConsoleLog { +export function isRemoteConsoleLog(obj: unknown): obj is IRemoteConsoleLog { const entry = obj as IRemoteConsoleLog; return entry && typeof entry.type === 'string' && typeof entry.severity === 'string'; diff --git a/src/vs/base/common/decorators/cancelPreviousCalls.ts b/src/vs/base/common/decorators/cancelPreviousCalls.ts index b0a3089bc31..7e901a24ca4 100644 --- a/src/vs/base/common/decorators/cancelPreviousCalls.ts +++ b/src/vs/base/common/decorators/cancelPreviousCalls.ts @@ -152,7 +152,7 @@ export function cancelPreviousCalls< const cancellationSource = new CancellationTokenSource(token); record.set(methodName, cancellationSource); - // then update or add cancelaltion token at the end of the arguments list + // then update or add cancellation token at the end of the arguments list if (CancellationToken.isCancellationToken(lastArgument)) { args[args.length - 1] = cancellationSource.token; } else { diff --git a/src/vs/base/common/diff/diff.ts b/src/vs/base/common/diff/diff.ts index ee2073099d6..17e307165da 100644 --- a/src/vs/base/common/diff/diff.ts +++ b/src/vs/base/common/diff/diff.ts @@ -94,7 +94,7 @@ class MyArray { * length: * A 64-bit integer that represents the number of elements to copy. */ - public static Copy(sourceArray: any[], sourceIndex: number, destinationArray: any[], destinationIndex: number, length: number) { + public static Copy(sourceArray: unknown[], sourceIndex: number, destinationArray: unknown[], destinationIndex: number, length: number) { for (let i = 0; i < length; i++) { destinationArray[destinationIndex + i] = sourceArray[sourceIndex + i]; } diff --git a/src/vs/base/common/hash.ts b/src/vs/base/common/hash.ts index 78697a888d7..366488e4567 100644 --- a/src/vs/base/common/hash.ts +++ b/src/vs/base/common/hash.ts @@ -18,7 +18,7 @@ export function hash(obj: T extends NotSyncHashable ? never : T): number { return doHash(obj, 0); } -export function doHash(obj: any, hashVal: number): number { +export function doHash(obj: unknown, hashVal: number): number { switch (typeof obj) { case 'object': if (obj === null) { @@ -93,7 +93,7 @@ export const hashAsync = (input: string | ArrayBufferView | VSBuffer) => { buff = input; } - return crypto.subtle.digest('sha-1', buff).then(toHexString); + return crypto.subtle.digest('sha-1', buff as ArrayBufferView).then(toHexString); }; const enum SHA1Constant { diff --git a/src/vs/base/common/htmlContent.ts b/src/vs/base/common/htmlContent.ts index fe1cbbb34d2..71087be10be 100644 --- a/src/vs/base/common/htmlContent.ts +++ b/src/vs/base/common/htmlContent.ts @@ -118,7 +118,7 @@ export function isEmptyMarkdownString(oneOrMany: IMarkdownString | IMarkdownStri } } -export function isMarkdownString(thing: any): thing is IMarkdownString { +export function isMarkdownString(thing: unknown): thing is IMarkdownString { if (thing instanceof MarkdownString) { return true; } else if (thing && typeof thing === 'object') { diff --git a/src/vs/base/common/iterator.ts b/src/vs/base/common/iterator.ts index 2ec62fe5c67..74f2ae00c7f 100644 --- a/src/vs/base/common/iterator.ts +++ b/src/vs/base/common/iterator.ts @@ -7,8 +7,8 @@ import { isIterable } from './types.js'; export namespace Iterable { - export function is(thing: any): thing is Iterable { - return thing && typeof thing === 'object' && typeof thing[Symbol.iterator] === 'function'; + export function is(thing: unknown): thing is Iterable { + return !!thing && typeof thing === 'object' && typeof (thing as Iterable)[Symbol.iterator] === 'function'; } const _empty: Iterable = Object.freeze([]); diff --git a/src/vs/base/common/json.ts b/src/vs/base/common/json.ts index e4adc59003e..8623e87503b 100644 --- a/src/vs/base/common/json.ts +++ b/src/vs/base/common/json.ts @@ -848,7 +848,7 @@ export function parse(text: string, errors: ParseError[] = [], options: ParseOpt let currentParent: any = []; const previousParents: any[] = []; - function onValue(value: any) { + function onValue(value: unknown) { if (Array.isArray(currentParent)) { (currentParent).push(value); } else if (currentProperty !== null) { @@ -929,7 +929,7 @@ export function parseTree(text: string, errors: ParseError[] = [], options: Pars currentParent = currentParent.parent!; ensurePropertyComplete(offset + length); }, - onLiteralValue: (value: any, offset: number, length: number) => { + onLiteralValue: (value: unknown, offset: number, length: number) => { onValue({ type: getNodeType(value), offset, length, parent: currentParent, value }); ensurePropertyComplete(offset + length); }, @@ -1308,7 +1308,7 @@ export function visit(text: string, visitor: JSONVisitor, options: ParseOptions return true; } -export function getNodeType(value: any): NodeType { +export function getNodeType(value: unknown): NodeType { switch (typeof value) { case 'boolean': return 'boolean'; case 'number': return 'number'; diff --git a/src/vs/base/common/jsonFormatter.ts b/src/vs/base/common/jsonFormatter.ts index 888daacea83..2e431ca8db8 100644 --- a/src/vs/base/common/jsonFormatter.ts +++ b/src/vs/base/common/jsonFormatter.ts @@ -207,7 +207,7 @@ export function format(documentText: string, range: Range | undefined, options: * @param any The object to stringify and format * @param options The formatting options to use */ -export function toFormattedString(obj: any, options: FormattingOptions) { +export function toFormattedString(obj: unknown, options: FormattingOptions) { const content = JSON.stringify(obj, undefined, options.insertSpaces ? options.tabSize || 4 : '\t'); if (options.eol !== undefined) { return content.replace(/\r\n|\r|\n/g, options.eol); diff --git a/src/vs/base/common/jsonSchema.ts b/src/vs/base/common/jsonSchema.ts index 77524a12438..6932da1b2d6 100644 --- a/src/vs/base/common/jsonSchema.ts +++ b/src/vs/base/common/jsonSchema.ts @@ -207,7 +207,7 @@ export function getCompressedContent(schema: IJSONSchema): string { type IJSONSchemaRef = IJSONSchema | boolean; -function isObject(thing: any): thing is object { +function isObject(thing: unknown): thing is object { return typeof thing === 'object' && thing !== null; } diff --git a/src/vs/base/common/marshalling.ts b/src/vs/base/common/marshalling.ts index 82fcd1234d0..1fd5c7fc2dd 100644 --- a/src/vs/base/common/marshalling.ts +++ b/src/vs/base/common/marshalling.ts @@ -7,7 +7,7 @@ import { VSBuffer } from './buffer.js'; import { URI, UriComponents } from './uri.js'; import { MarshalledId } from './marshallingIds.js'; -export function stringify(obj: any): string { +export function stringify(obj: unknown): string { return JSON.stringify(obj, replacer); } diff --git a/src/vs/base/common/objectCache.ts b/src/vs/base/common/objectCache.ts index 09be29ccd45..aa50e02f5c6 100644 --- a/src/vs/base/common/objectCache.ts +++ b/src/vs/base/common/objectCache.ts @@ -83,7 +83,7 @@ export class ObjectCache< this._register(new DisposableMap()); constructor( - private readonly factory: (key: TKey) => TValue & { disposed: false }, + private readonly factory: (key: TKey) => TValue & { isDisposed: false }, ) { super(); } @@ -96,11 +96,11 @@ export class ObjectCache< * @throws if {@linkcode factory} callback returns a disposed object. * @param key - ID of the object in the cache */ - public get(key: TKey): TValue & { disposed: false } { + public get(key: TKey): TValue & { isDisposed: false } { let object = this.cache.get(key); // if object is already disposed, remove it from the cache - if (object?.disposed) { + if (object?.isDisposed) { this.cache.deleteAndLeak(key); object = undefined; } @@ -126,7 +126,7 @@ export class ObjectCache< ); // remove it from the cache automatically on dispose - object.addDisposable( + object.addDisposables( object.onDispose(() => { this.cache.deleteAndLeak(key); })); diff --git a/src/vs/base/common/observableDisposable.ts b/src/vs/base/common/observableDisposable.ts index 8050479c05f..41b7a714ddf 100644 --- a/src/vs/base/common/observableDisposable.ts +++ b/src/vs/base/common/observableDisposable.ts @@ -3,19 +3,25 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Emitter } from './event.js'; -import { Disposable, IDisposable } from './lifecycle.js'; +import { Disposable, DisposableStore, IDisposable, toDisposable } from './lifecycle.js'; /** - * Disposable object that tracks its {@linkcode disposed} state + * Disposable object that tracks its {@linkcode isDisposed} state * as a public attribute and provides the {@linkcode onDispose} * event to subscribe to. */ export abstract class ObservableDisposable extends Disposable { /** - * Private emitter for the `onDispose` event. + * Underlying disposables store this object relies on. */ - private readonly _onDispose = this._register(new Emitter()); + private readonly store = this._register(new DisposableStore()); + + /** + * Check if the current object is already has been disposed. + */ + public get isDisposed(): boolean { + return this.store.isDisposed; + } /** * The event is fired when this object is disposed. @@ -25,57 +31,29 @@ export abstract class ObservableDisposable extends Disposable { */ public onDispose(callback: () => void): IDisposable { // if already disposed, execute the callback immediately - if (this.disposed) { + if (this.isDisposed) { const timeoutHandle = setTimeout(callback); - return { - dispose: () => { - clearTimeout(timeoutHandle); - }, - }; + return toDisposable(() => { + clearTimeout(timeoutHandle); + }); } - return this._onDispose.event(callback); + return this.store.add(toDisposable(callback)); } /** - * Adds a disposable object to the list of disposables + * Adds disposable object(s) to the list of disposables * that will be disposed with this object. */ - public addDisposable(...disposables: IDisposable[]): this { + public addDisposables(...disposables: IDisposable[]): this { for (const disposable of disposables) { - this._register(disposable); + this.store.add(disposable); } return this; } - /** - * Tracks 'disposed' state of this object. - */ - private _disposed = false; - - /** - * Gets current 'disposed' state of this object. - */ - public get disposed(): boolean { - return this._disposed; - } - - /** - * Dispose current object if not already disposed. - * @returns - */ - public override dispose(): void { - if (this.disposed) { - return; - } - this._disposed = true; - - this._onDispose.fire(); - super.dispose(); - } - /** * Assert that the current object was not yet disposed. * @@ -92,7 +70,7 @@ export abstract class ObservableDisposable extends Disposable { /** * Type for a non-disposed object `TObject`. */ -type TNotDisposed = TObject & { disposed: false }; +type TNotDisposed = TObject & { isDisposed: false }; /** * Asserts that a provided `object` is not `disposed` yet, @@ -101,11 +79,11 @@ type TNotDisposed = TObject & { disposed: * @throws if the provided `object.disposed` equal to `false`. * @param error Error message or error object to throw if assertion fails. */ -export function assertNotDisposed( +export function assertNotDisposed( object: TObject, error: string | Error, ): asserts object is TNotDisposed { - if (!object.disposed) { + if (!object.isDisposed) { return; } diff --git a/src/vs/base/common/observableInternal/autorun.ts b/src/vs/base/common/observableInternal/autorun.ts index 25055082928..3eb91fbc964 100644 --- a/src/vs/base/common/observableInternal/autorun.ts +++ b/src/vs/base/common/observableInternal/autorun.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IObservable, IObservableWithChange, IObserver, IReader } from './base.js'; +import { IObservable, IObservableWithChange, IObserver, IReader, IReaderWithStore } from './base.js'; import { DebugNameData, IDebugNameData } from './debugName.js'; import { assertFn, BugIndicatingError, DisposableStore, IDisposable, markAsDisposed, onBugIndicatingError, toDisposable, trackDisposable } from './commonFacade/deps.js'; import { getLogger } from './logging/logging.js'; @@ -13,7 +13,7 @@ import { IChangeTracker } from './changeTracker.js'; * Runs immediately and whenever a transaction ends and an observed observable changed. * {@link fn} should start with a JS Doc using `@description` to name the autorun. */ -export function autorun(fn: (reader: IReader) => void): IDisposable { +export function autorun(fn: (reader: IReaderWithStore) => void): IDisposable { return new AutorunObserver( new DebugNameData(undefined, undefined, fn), fn, @@ -25,7 +25,7 @@ export function autorun(fn: (reader: IReader) => void): IDisposable { * Runs immediately and whenever a transaction ends and an observed observable changed. * {@link fn} should start with a JS Doc using `@description` to name the autorun. */ -export function autorunOpts(options: IDebugNameData & {}, fn: (reader: IReader) => void): IDisposable { +export function autorunOpts(options: IDebugNameData & {}, fn: (reader: IReaderWithStore) => void): IDisposable { return new AutorunObserver( new DebugNameData(options.owner, options.debugName, options.debugReferenceFn ?? fn), fn, @@ -87,6 +87,8 @@ export function autorunWithStoreHandleChanges( /** * @see autorun (but with a disposable store that is cleared before the next run or on dispose) + * + * @deprecated Use `autorun(reader => { reader.store.add(...) })` instead! */ export function autorunWithStore(fn: (reader: IReader, store: DisposableStore) => void): IDisposable { const store = new DisposableStore(); @@ -162,7 +164,7 @@ export const enum AutorunState { upToDate = 3, } -export class AutorunObserver implements IObserver, IReader, IDisposable { +export class AutorunObserver implements IObserver, IReaderWithStore, IDisposable { private _state = AutorunState.stale; private _updateCount = 0; private _disposed = false; @@ -177,7 +179,7 @@ export class AutorunObserver implements IObserver, IReader constructor( public readonly _debugNameData: DebugNameData, - public readonly _runFn: (reader: IReader, changeSummary: TChangeSummary) => void, + public readonly _runFn: (reader: IReaderWithStore, changeSummary: TChangeSummary) => void, private readonly _changeTracker: IChangeTracker | undefined, ) { this._changeSummary = this._changeTracker?.createChangeSummary(undefined); @@ -197,6 +199,13 @@ export class AutorunObserver implements IObserver, IReader } this._dependencies.clear(); + if (this._store !== undefined) { + this._store.dispose(); + } + if (this._delayedStore !== undefined) { + this._delayedStore.dispose(); + } + getLogger()?.handleAutorunDisposed(this); markAsDisposed(this); } @@ -212,17 +221,28 @@ export class AutorunObserver implements IObserver, IReader if (!this._disposed) { getLogger()?.handleAutorunStarted(this); const changeSummary = this._changeSummary!; + const delayedStore = this._delayedStore; + if (delayedStore !== undefined) { + this._delayedStore = undefined; + } try { this._isRunning = true; if (this._changeTracker) { this._changeTracker.beforeUpdate?.(this, changeSummary); this._changeSummary = this._changeTracker.createChangeSummary(changeSummary); // Warning: external call! } + if (this._store !== undefined) { + this._store.clear(); + } + this._runFn(this, changeSummary); // Warning: external call! } catch (e) { onBugIndicatingError(e); } finally { this._isRunning = false; + if (delayedStore !== undefined) { + delayedStore.dispose(); + } } } } finally { @@ -308,8 +328,12 @@ export class AutorunObserver implements IObserver, IReader // IReader implementation - public readObservable(observable: IObservable): T { + private _ensureNoRunning(): void { if (!this._isRunning) { throw new BugIndicatingError('The reader object cannot be used outside its compute function!'); } + } + + public readObservable(observable: IObservable): T { + this._ensureNoRunning(); // In case the run action disposes the autorun if (this._disposed) { @@ -323,6 +347,32 @@ export class AutorunObserver implements IObserver, IReader return value; } + private _store: DisposableStore | undefined = undefined; + get store(): DisposableStore { + this._ensureNoRunning(); + if (this._disposed) { + throw new BugIndicatingError('Cannot access store after dispose'); + } + + if (this._store === undefined) { + this._store = new DisposableStore(); + } + return this._store; + } + + private _delayedStore: DisposableStore | undefined = undefined; + get delayedStore(): DisposableStore { + this._ensureNoRunning(); + if (this._disposed) { + throw new BugIndicatingError('Cannot access store after dispose'); + } + + if (this._delayedStore === undefined) { + this._delayedStore = new DisposableStore(); + } + return this._delayedStore; + } + public debugGetState() { return { isRunning: this._isRunning, diff --git a/src/vs/base/common/observableInternal/base.ts b/src/vs/base/common/observableInternal/base.ts index 94c454d75ab..37bd3c66c12 100644 --- a/src/vs/base/common/observableInternal/base.ts +++ b/src/vs/base/common/observableInternal/base.ts @@ -570,3 +570,19 @@ export class DisposableObservableValue extends IReader { +export interface IDerivedReader extends IReaderWithStore { /** * Call this to report a change delta or to force report a change, even if the new value is the same as the old value. */ @@ -101,7 +101,13 @@ export function derivedHandleChanges( ); } +/** + * @deprecated Use `derived(reader => { reader.store.add(...) })` instead! +*/ export function derivedWithStore(computeFn: (reader: IReader, store: DisposableStore) => T): IObservable; +/** + * @deprecated Use `derived(reader => { reader.store.add(...) })` instead! +*/ export function derivedWithStore(owner: DebugOwner, computeFn: (reader: IReader, store: DisposableStore) => T): IObservable; export function derivedWithStore(computeFnOrOwner: ((reader: IReader, store: DisposableStore) => T) | DebugOwner, computeFnOrUndefined?: ((reader: IReader, store: DisposableStore) => T)): IObservable { let computeFn: (reader: IReader, store: DisposableStore) => T; @@ -234,6 +240,15 @@ export class Derived extends BaseObserv } this._dependencies.clear(); + if (this._store !== undefined) { + this._store.dispose(); + this._store = undefined; + } + if (this._delayedStore !== undefined) { + this._delayedStore.dispose(); + this._delayedStore = undefined; + } + this._handleLastObserverRemoved?.(); } @@ -310,14 +325,22 @@ export class Derived extends BaseObserv try { const changeSummary = this._changeSummary!; + const delayedStore = this._delayedStore; + if (delayedStore !== undefined) { + this._delayedStore = undefined; + } try { this._isReaderValid = true; if (this._changeTracker) { this._changeTracker.beforeUpdate?.(this, changeSummary); this._changeSummary = this._changeTracker?.createChangeSummary(changeSummary); } + if (this._store !== undefined) { + this._store.clear(); + } /** might call {@link handleChange} indirectly, which could invalidate us */ this._value = this._computeFn(this, changeSummary); + } finally { this._isReaderValid = false; // We don't want our observed observables to think that they are (not even temporarily) not being observed. @@ -326,6 +349,10 @@ export class Derived extends BaseObserv o.removeObserver(this); } this._dependenciesToBeRemoved.clear(); + + if (delayedStore !== undefined) { + delayedStore.dispose(); + } } didChange = this._didReportChange || (hadValue && !(this._equalityComparator(oldValue!, this._value))); @@ -447,8 +474,12 @@ export class Derived extends BaseObserv // IReader Implementation private _isReaderValid = false; - public readObservable(observable: IObservable): T { + private _ensureNoRunning(): void { if (!this._isReaderValid) { throw new BugIndicatingError('The reader object cannot be used outside its compute function!'); } + } + + public readObservable(observable: IObservable): T { + this._ensureNoRunning(); // Subscribe before getting the value to enable caching observable.addObserver(this); @@ -461,7 +492,7 @@ export class Derived extends BaseObserv } public reportChange(change: TChange): void { - if (!this._isReaderValid) { throw new BugIndicatingError('The reader object cannot be used outside its compute function!'); } + this._ensureNoRunning(); this._didReportChange = true; // TODO add logging @@ -470,6 +501,26 @@ export class Derived extends BaseObserv } } + private _store: DisposableStore | undefined = undefined; + get store(): DisposableStore { + this._ensureNoRunning(); + + if (this._store === undefined) { + this._store = new DisposableStore(); + } + return this._store; + } + + private _delayedStore: DisposableStore | undefined = undefined; + get delayedStore(): DisposableStore { + this._ensureNoRunning(); + + if (this._delayedStore === undefined) { + this._delayedStore = new DisposableStore(); + } + return this._delayedStore; + } + public override addObserver(observer: IObserver): void { const shouldCallBeginUpdate = !this._observers.has(observer) && this._updateCount > 0; super.addObserver(observer); diff --git a/src/vs/base/common/product.ts b/src/vs/base/common/product.ts index 2c50e7512b5..ff58a24eaf2 100644 --- a/src/vs/base/common/product.ts +++ b/src/vs/base/common/product.ts @@ -338,6 +338,8 @@ export interface IDefaultChatAgent { readonly providerName: string; readonly enterpriseProviderId: string; readonly enterpriseProviderName: string; + readonly alternativeProviderId: string; + readonly alternativeProviderName: string; readonly providerUriSetting: string; readonly providerScopes: string[][]; @@ -351,6 +353,7 @@ export interface IDefaultChatAgent { readonly completionsMenuCommand: string; readonly completionsRefreshTokenCommand: string; readonly chatRefreshTokenCommand: string; + readonly generateCommitMessageCommand: string; readonly completionsAdvancedSetting: string; readonly completionsEnablementSetting: string; diff --git a/src/vs/base/common/resourceTree.ts b/src/vs/base/common/resourceTree.ts index 44cb55a065e..c1a1c951bb2 100644 --- a/src/vs/base/common/resourceTree.ts +++ b/src/vs/base/common/resourceTree.ts @@ -91,7 +91,7 @@ export class ResourceTree, C> { return collect(node, []); } - static isResourceNode(obj: any): obj is IResourceNode { + static isResourceNode(obj: unknown): obj is IResourceNode { return obj instanceof Node; } diff --git a/src/vs/base/common/strings.ts b/src/vs/base/common/strings.ts index 1b12d1a4212..5d4d135ca89 100644 --- a/src/vs/base/common/strings.ts +++ b/src/vs/base/common/strings.ts @@ -102,7 +102,7 @@ export function count(value: string, substr: string): number { return result; } -export function truncate(value: string, maxLength: number, suffix = '…'): string { +export function truncate(value: string, maxLength: number, suffix = Ellipsis): string { if (value.length <= maxLength) { return value; } @@ -110,7 +110,7 @@ export function truncate(value: string, maxLength: number, suffix = '…'): stri return `${value.substr(0, maxLength)}${suffix}`; } -export function truncateMiddle(value: string, maxLength: number, suffix = '…'): string { +export function truncateMiddle(value: string, maxLength: number, suffix = Ellipsis): string { if (value.length <= maxLength) { return value; } @@ -1359,3 +1359,5 @@ export class InvisibleCharacters { return InvisibleCharacters.getData(); } } + +export const Ellipsis = '\u2026'; diff --git a/src/vs/base/common/themables.ts b/src/vs/base/common/themables.ts index a9273744ded..6b2551bebb2 100644 --- a/src/vs/base/common/themables.ts +++ b/src/vs/base/common/themables.ts @@ -14,8 +14,8 @@ export interface ThemeColor { } export namespace ThemeColor { - export function isThemeColor(obj: any): obj is ThemeColor { - return obj && typeof obj === 'object' && typeof (obj).id === 'string'; + export function isThemeColor(obj: unknown): obj is ThemeColor { + return !!obj && typeof obj === 'object' && typeof (obj).id === 'string'; } } @@ -58,8 +58,8 @@ export namespace ThemeIcon { return '.' + asClassNameArray(icon).join('.'); } - export function isThemeIcon(obj: any): obj is ThemeIcon { - return obj && typeof obj === 'object' && typeof (obj).id === 'string' && (typeof (obj).color === 'undefined' || ThemeColor.isThemeColor((obj).color)); + export function isThemeIcon(obj: unknown): obj is ThemeIcon { + return !!obj && typeof obj === 'object' && typeof (obj).id === 'string' && (typeof (obj).color === 'undefined' || ThemeColor.isThemeColor((obj).color)); } const _regexFromString = new RegExp(`^\\$\\((${ThemeIcon.iconNameExpression}(?:${ThemeIcon.iconModifierExpression})?)\\)$`); diff --git a/src/vs/base/common/uri.ts b/src/vs/base/common/uri.ts index f04f368a27d..ac2ae962d72 100644 --- a/src/vs/base/common/uri.ts +++ b/src/vs/base/common/uri.ts @@ -427,7 +427,7 @@ export interface UriComponents { fragment?: string; } -export function isUriComponents(thing: any): thing is UriComponents { +export function isUriComponents(thing: unknown): thing is UriComponents { if (!thing || typeof thing !== 'object') { return false; } diff --git a/src/vs/base/parts/ipc/common/ipc.net.ts b/src/vs/base/parts/ipc/common/ipc.net.ts index 1bc63ba6878..bf5ae2b944a 100644 --- a/src/vs/base/parts/ipc/common/ipc.net.ts +++ b/src/vs/base/parts/ipc/common/ipc.net.ts @@ -66,7 +66,7 @@ export namespace SocketDiagnostics { const socketIds = new WeakMap(); let lastUsedSocketId = 0; - function getSocketId(nativeObject: any, label: string): string { + function getSocketId(nativeObject: unknown, label: string): string { if (!socketIds.has(nativeObject)) { const id = String(++lastUsedSocketId); socketIds.set(nativeObject, id); @@ -74,7 +74,7 @@ export namespace SocketDiagnostics { return socketIds.get(nativeObject)!; } - export function traceSocketEvent(nativeObject: any, socketDebugLabel: string, type: SocketDiagnosticsEventType, data?: VSBuffer | Uint8Array | ArrayBuffer | ArrayBufferView | any): void { + export function traceSocketEvent(nativeObject: unknown, socketDebugLabel: string, type: SocketDiagnosticsEventType, data?: VSBuffer | Uint8Array | ArrayBuffer | ArrayBufferView | any): void { if (!enableDiagnostics) { return; } diff --git a/src/vs/base/parts/ipc/common/ipc.ts b/src/vs/base/parts/ipc/common/ipc.ts index 92bb92c78c6..3e65fecfc71 100644 --- a/src/vs/base/parts/ipc/common/ipc.ts +++ b/src/vs/base/parts/ipc/common/ipc.ts @@ -368,7 +368,7 @@ export class ChannelServer implements IChannelServer & { disposed: false }; + return result as TestObject & { isDisposed: false }; }; // ObjectCache diff --git a/src/vs/base/test/common/observable.test.ts b/src/vs/base/test/common/observable.test.ts index 6b640a275bb..16ed216df98 100644 --- a/src/vs/base/test/common/observable.test.ts +++ b/src/vs/base/test/common/observable.test.ts @@ -6,7 +6,7 @@ import assert from 'assert'; import { setUnexpectedErrorHandler } from '../../common/errors.js'; import { Emitter, Event } from '../../common/event.js'; -import { DisposableStore } from '../../common/lifecycle.js'; +import { DisposableStore, toDisposable } from '../../common/lifecycle.js'; import { IDerivedReader, IObservableWithChange, autorun, autorunHandleChanges, autorunWithStoreHandleChanges, derived, derivedDisposable, IObservable, IObserver, ISettableObservable, ITransaction, keepObserved, observableFromEvent, observableSignal, observableValue, recordChanges, transaction, waitForState } from '../../common/observable.js'; // eslint-disable-next-line local/code-no-deep-import-of-internal import { BaseObservable } from '../../common/observableInternal/base.js'; @@ -1613,6 +1613,83 @@ suite('observables', () => { ])); }); }); + + suite('disposableStores', () => { + test('derived with store', () => { + const log = new Log(); + const observable1 = observableValue('myObservableValue1', 0); + + const computed1 = derived((reader) => { + const value = observable1.read(reader); + log.log(`computed ${value}`); + reader.store.add(toDisposable(() => { + log.log(`computed1: ${value} disposed`); + })); + return value; + }); + + const a = autorun(reader => { + log.log(`a: ${computed1.read(reader)}`); + }); + + assert.deepStrictEqual(log.getAndClearEntries(), ([ + "computed 0", + "a: 0" + ])); + + observable1.set(1, undefined); + + assert.deepStrictEqual(log.getAndClearEntries(), ([ + "computed1: 0 disposed", + "computed 1", + "a: 1" + ])); + + a.dispose(); + + assert.deepStrictEqual(log.getAndClearEntries(), ([ + "computed1: 1 disposed" + ])); + }); + + test('derived with delayedStore', () => { + const log = new Log(); + const observable1 = observableValue('myObservableValue1', 0); + + const computed1 = derived((reader) => { + const value = observable1.read(reader); + log.log(`computed ${value}`); + reader.delayedStore.add(toDisposable(() => { + log.log(`computed1: ${value} disposed`); + })); + return value; + }); + + const a = autorun(reader => { + log.log(`a: ${computed1.read(reader)}`); + }); + + assert.deepStrictEqual(log.getAndClearEntries(), ([ + "computed 0", + "a: 0" + ])); + + observable1.set(1, undefined); + + assert.deepStrictEqual(log.getAndClearEntries(), ([ + "computed 1", + "computed1: 0 disposed", + "a: 1" + ])); + + a.dispose(); + + assert.deepStrictEqual(log.getAndClearEntries(), ([ + "computed1: 1 disposed" + ])); + }); + }); + }); export class LoggingObserver implements IObserver { diff --git a/src/vs/base/test/common/observableDisposable.test.ts b/src/vs/base/test/common/observableDisposable.test.ts index 03768cb246f..afc90077fd8 100644 --- a/src/vs/base/test/common/observableDisposable.test.ts +++ b/src/vs/base/test/common/observableDisposable.test.ts @@ -30,14 +30,14 @@ suite('ObservableDisposable', () => { ); assert( - object.disposed === false, + object.isDisposed === false, 'Object must not be disposed yet.', ); object.dispose(); assert( - object.disposed, + object.isDisposed, 'Object must be disposed.', ); }); @@ -50,11 +50,11 @@ suite('ObservableDisposable', () => { disposables.add(object); assert( - object.disposed === false, + object.isDisposed === false, 'Object must not be disposed yet.', ); - const onDisposeSpy = spy(() => { }); + const onDisposeSpy = spy(); disposables.add(object.onDispose(onDisposeSpy)); assert( @@ -78,7 +78,7 @@ suite('ObservableDisposable', () => { */ assert( - object.disposed, + object.isDisposed, 'Object must be disposed.', ); @@ -102,7 +102,7 @@ suite('ObservableDisposable', () => { ); assert( - object.disposed, + object.isDisposed, 'Object must be disposed.', ); }); @@ -168,7 +168,7 @@ suite('ObservableDisposable', () => { disposables.add(object); assert( - object.disposed === false, + object.isDisposed === false, 'Object must not be disposed yet.', ); @@ -185,7 +185,7 @@ suite('ObservableDisposable', () => { ); } - object.addDisposable(...disposableObjects); + object.addDisposables(...disposableObjects); // a sanity check after the 'addDisposable' call for (const disposable of disposableObjects) { @@ -228,7 +228,7 @@ suite('ObservableDisposable', () => { const disposableObject = new TestDisposable(); allDisposables.push(disposableObject); if (parent !== null) { - parent.addDisposable(disposableObject); + parent.addDisposables(disposableObject); } // generate child disposable objects recursively @@ -256,7 +256,7 @@ suite('ObservableDisposable', () => { disposables.add(object); assert( - object.disposed === false, + object.isDisposed === false, 'Object must not be disposed yet.', ); @@ -271,7 +271,7 @@ suite('ObservableDisposable', () => { // a sanity check for the initial state of the objects for (const disposable of allDisposableObjects) { assert( - disposable.disposed === false, + disposable.isDisposed === false, 'Disposable object must not be disposed yet.', ); } @@ -280,7 +280,7 @@ suite('ObservableDisposable', () => { // finally validate that all objects are disposed const allDisposed = allDisposableObjects.reduce((acc, disposable) => { - return acc && disposable.disposed; + return acc && disposable.isDisposed; }, true); assert( diff --git a/src/vs/base/test/common/snapshot.ts b/src/vs/base/test/common/snapshot.ts index bdde1f747f4..9523d6ca3aa 100644 --- a/src/vs/base/test/common/snapshot.ts +++ b/src/vs/base/test/common/snapshot.ts @@ -51,7 +51,7 @@ export class SnapshotContext { this.snapshotsDir = URI.joinPath(src, ...[...parts.slice(0, -1), '__snapshots__']); } - public async assert(value: any, options?: ISnapshotOptions) { + public async assert(value: unknown, options?: ISnapshotOptions) { const originalStack = new Error().stack!; // save to make the stack nicer on failure const nameOrIndex = (options?.name ? sanitizeName(options.name) : this.nextIndex++); const fileName = this.namePrefix + nameOrIndex + '.' + (options?.extension || 'snap'); @@ -176,7 +176,7 @@ teardown(async function () { * in a `__snapshots__` directory next to the test file, which is expected to * be the first `.test.js` file in the callstack. */ -export function assertSnapshot(value: any, options?: ISnapshotOptions): Promise { +export function assertSnapshot(value: unknown, options?: ISnapshotOptions): Promise { if (!context) { throw new Error('assertSnapshot can only be used in a test'); } diff --git a/src/vs/base/test/common/testUtils.ts b/src/vs/base/test/common/testUtils.ts index e184841e421..f7b3f65fa26 100644 --- a/src/vs/base/test/common/testUtils.ts +++ b/src/vs/base/test/common/testUtils.ts @@ -22,8 +22,7 @@ export function flakySuite(title: string, fn: () => void) /* Suite */ { } /** - * Helper function that allows to await for a specified amount of time. - * @param ms The amount of time to wait in milliseconds. + * @deprecated use `async#timeout` instead */ export const wait = (ms: number): Promise => { return new Promise(resolve => setTimeout(resolve, ms)); @@ -53,13 +52,7 @@ export const randomBoolean = (): boolean => { }; /** - * Mocks an `TObject` with the provided `overrides`. - * - * If you need to mock an `Service`, please use {@link mockService} - * instead which provides better type safety guarantees for the case. - * - * @throws Reading non-overridden property or function - * on `TObject` throws an error. + *@deprecated use `mock.ts#mock` instead */ export function mockObject( overrides: Partial, diff --git a/src/vs/code/browser/workbench/workbench.ts b/src/vs/code/browser/workbench/workbench.ts index ab9671491fb..58d187faea8 100644 --- a/src/vs/code/browser/workbench/workbench.ts +++ b/src/vs/code/browser/workbench/workbench.ts @@ -109,9 +109,9 @@ class ServerKeyedAESCrypto implements ISecretStorageCrypto { // Do the decryption and parse the result as JSON const key = await this.getKey(clientKey.buffer); const decrypted = await mainWindow.crypto.subtle.decrypt( - { name: AESConstants.ALGORITHM as const, iv: iv.buffer }, + { name: AESConstants.ALGORITHM as const, iv: iv.buffer as Uint8Array }, key, - cipherText.buffer + cipherText.buffer as Uint8Array ); return new TextDecoder().decode(new Uint8Array(decrypted)); diff --git a/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts b/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts index 61a913ae7bc..12132e26720 100644 --- a/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts +++ b/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts @@ -24,7 +24,7 @@ import { Range } from '../../../../common/core/range.js'; import { Selection } from '../../../../common/core/selection.js'; import { Position } from '../../../../common/core/position.js'; import { IVisibleRangeProvider } from '../textArea/textAreaEditContext.js'; -import { PositionOffsetTransformer } from '../../../../common/core/positionToOffset.js'; +import { PositionOffsetTransformer } from '../../../../common/core/text/positionToOffset.js'; import { IDisposable, MutableDisposable } from '../../../../../base/common/lifecycle.js'; import { EditContext } from './editContextFactory.js'; import { IAccessibilityService } from '../../../../../platform/accessibility/common/accessibility.js'; diff --git a/src/vs/editor/browser/editorBrowser.ts b/src/vs/editor/browser/editorBrowser.ts index 69b6021983e..d63387889fe 100644 --- a/src/vs/editor/browser/editorBrowser.ts +++ b/src/vs/editor/browser/editorBrowser.ts @@ -1078,7 +1078,7 @@ export interface ICodeEditor extends editorCommon.IEditor { /** * Get the line height for the line number. */ - getLineHeightForLineNumber(lineNumber: number): number; + getLineHeightForPosition(position: IPosition): number; /** * Set the model ranges that will be hidden in the view. diff --git a/src/vs/editor/browser/gpu/gpuDisposable.ts b/src/vs/editor/browser/gpu/gpuDisposable.ts index 1405eb898d2..875525be3ee 100644 --- a/src/vs/editor/browser/gpu/gpuDisposable.ts +++ b/src/vs/editor/browser/gpu/gpuDisposable.ts @@ -28,7 +28,7 @@ export namespace GPULifecycle { export function createBuffer(device: GPUDevice, descriptor: GPUBufferDescriptor, initialValues?: Float32Array | (() => Float32Array)): IReference { const buffer = device.createBuffer(descriptor); if (initialValues) { - device.queue.writeBuffer(buffer, 0, isFunction(initialValues) ? initialValues() : initialValues); + device.queue.writeBuffer(buffer, 0, (isFunction(initialValues) ? initialValues() : initialValues) as Float32Array); } return wrapDestroyableInDisposable(buffer); } diff --git a/src/vs/editor/browser/gpu/rectangleRenderer.ts b/src/vs/editor/browser/gpu/rectangleRenderer.ts index 5be1db2f162..09e68a80be8 100644 --- a/src/vs/editor/browser/gpu/rectangleRenderer.ts +++ b/src/vs/editor/browser/gpu/rectangleRenderer.ts @@ -251,7 +251,7 @@ export class RectangleRenderer extends ViewEventHandler { const dpr = getActiveWindow().devicePixelRatio; this._scrollOffsetValueBuffer[0] = this._context.viewLayout.getCurrentScrollLeft() * dpr; this._scrollOffsetValueBuffer[1] = this._context.viewLayout.getCurrentScrollTop() * dpr; - this._device.queue.writeBuffer(this._scrollOffsetBindBuffer, 0, this._scrollOffsetValueBuffer); + this._device.queue.writeBuffer(this._scrollOffsetBindBuffer, 0, this._scrollOffsetValueBuffer as Float32Array); } return true; } diff --git a/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts index c41efda089b..6db8ab9e476 100644 --- a/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts @@ -174,7 +174,7 @@ export class FullFileRenderStrategy extends BaseRenderStrategy { const dpr = getActiveWindow().devicePixelRatio; this._scrollOffsetValueBuffer[0] = (e?.scrollLeft ?? this._context.viewLayout.getCurrentScrollLeft()) * dpr; this._scrollOffsetValueBuffer[1] = (e?.scrollTop ?? this._context.viewLayout.getCurrentScrollTop()) * dpr; - this._device.queue.writeBuffer(this._scrollOffsetBindBuffer, 0, this._scrollOffsetValueBuffer); + this._device.queue.writeBuffer(this._scrollOffsetBindBuffer, 0, this._scrollOffsetValueBuffer as Float32Array); return true; } diff --git a/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts b/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts index f35ccc85edc..d051c007716 100644 --- a/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts @@ -157,7 +157,7 @@ export class ViewportRenderStrategy extends BaseRenderStrategy { const dpr = getActiveWindow().devicePixelRatio; this._scrollOffsetValueBuffer[0] = (e?.scrollLeft ?? this._context.viewLayout.getCurrentScrollLeft()) * dpr; this._scrollOffsetValueBuffer[1] = (e?.scrollTop ?? this._context.viewLayout.getCurrentScrollTop()) * dpr; - this._device.queue.writeBuffer(this._scrollOffsetBindBuffer, 0, this._scrollOffsetValueBuffer); + this._device.queue.writeBuffer(this._scrollOffsetBindBuffer, 0, this._scrollOffsetValueBuffer as Float32Array); return true; } diff --git a/src/vs/editor/browser/observableCodeEditor.ts b/src/vs/editor/browser/observableCodeEditor.ts index 1bb9fc7318c..487a4326925 100644 --- a/src/vs/editor/browser/observableCodeEditor.ts +++ b/src/vs/editor/browser/observableCodeEditor.ts @@ -8,7 +8,7 @@ import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../ba import { IObservable, IObservableWithChange, ITransaction, TransactionImpl, autorun, autorunOpts, derived, derivedOpts, derivedWithSetter, observableFromEvent, observableSignal, observableValue, observableValueOpts } from '../../base/common/observable.js'; import { EditorOption, FindComputedEditorOptionValueById } from '../common/config/editorOptions.js'; import { LineRange } from '../common/core/ranges/lineRange.js'; -import { OffsetRange } from '../common/core/offsetRange.js'; +import { OffsetRange } from '../common/core/ranges/offsetRange.js'; import { Position } from '../common/core/position.js'; import { Selection } from '../common/core/selection.js'; import { ICursorSelectionChangedEvent } from '../common/cursorEvents.js'; @@ -47,8 +47,8 @@ export class ObservableCodeEditor extends Disposable { return result; } - private _updateCounter = 0; - private _currentTransaction: TransactionImpl | undefined = undefined; + private _updateCounter; + private _currentTransaction: TransactionImpl | undefined; private _beginUpdate(): void { this._updateCounter++; @@ -70,6 +70,84 @@ export class ObservableCodeEditor extends Disposable { private constructor(public readonly editor: ICodeEditor) { super(); + this._updateCounter = 0; + this._currentTransaction = undefined; + this._model = observableValue(this, this.editor.getModel()); + this.model = this._model; + this.isReadonly = observableFromEvent(this, this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.readOnly)); + this._versionId = observableValueOpts({ owner: this, lazy: true }, this.editor.getModel()?.getVersionId() ?? null); + this.versionId = this._versionId; + this._selections = observableValueOpts( + { owner: this, equalsFn: equalsIfDefined(itemsEquals(Selection.selectionsEqual)), lazy: true }, + this.editor.getSelections() ?? null + ); + this.selections = this._selections; + this.positions = derivedOpts( + { owner: this, equalsFn: equalsIfDefined(itemsEquals(Position.equals)) }, + reader => this.selections.read(reader)?.map(s => s.getStartPosition()) ?? null + ); + this.isFocused = observableFromEvent(this, e => { + const d1 = this.editor.onDidFocusEditorWidget(e); + const d2 = this.editor.onDidBlurEditorWidget(e); + return { + dispose() { + d1.dispose(); + d2.dispose(); + } + }; + }, () => this.editor.hasWidgetFocus()); + this.isTextFocused = observableFromEvent(this, e => { + const d1 = this.editor.onDidFocusEditorText(e); + const d2 = this.editor.onDidBlurEditorText(e); + return { + dispose() { + d1.dispose(); + d2.dispose(); + } + }; + }, () => this.editor.hasTextFocus()); + this.inComposition = observableFromEvent(this, e => { + const d1 = this.editor.onDidCompositionStart(() => { + e(undefined); + }); + const d2 = this.editor.onDidCompositionEnd(() => { + e(undefined); + }); + return { + dispose() { + d1.dispose(); + d2.dispose(); + } + }; + }, () => this.editor.inComposition); + this.value = derivedWithSetter(this, + reader => { this.versionId.read(reader); return this.model.read(reader)?.getValue() ?? ''; }, + (value, tx) => { + const model = this.model.get(); + if (model !== null) { + if (value !== model.getValue()) { + model.setValue(value); + } + } + } + ); + this.valueIsEmpty = derived(this, reader => { this.versionId.read(reader); return this.editor.getModel()?.getValueLength() === 0; }); + this.cursorSelection = derivedOpts({ owner: this, equalsFn: equalsIfDefined(Selection.selectionsEqual) }, reader => this.selections.read(reader)?.[0] ?? null); + this.cursorPosition = derivedOpts({ owner: this, equalsFn: Position.equals }, reader => this.selections.read(reader)?.[0]?.getPosition() ?? null); + this.cursorLineNumber = derived(this, reader => this.cursorPosition.read(reader)?.lineNumber ?? null); + this.onDidType = observableSignal(this); + this.onDidPaste = observableSignal(this); + this.scrollTop = observableFromEvent(this.editor.onDidScrollChange, () => this.editor.getScrollTop()); + this.scrollLeft = observableFromEvent(this.editor.onDidScrollChange, () => this.editor.getScrollLeft()); + this.layoutInfo = observableFromEvent(this.editor.onDidLayoutChange, () => this.editor.getLayoutInfo()); + this.layoutInfoContentLeft = this.layoutInfo.map(l => l.contentLeft); + this.layoutInfoDecorationsLeft = this.layoutInfo.map(l => l.decorationsLeft); + this.layoutInfoWidth = this.layoutInfo.map(l => l.width); + this.layoutInfoMinimap = this.layoutInfo.map(l => l.minimap); + this.layoutInfoVerticalScrollbarWidth = this.layoutInfo.map(l => l.verticalScrollbarWidth); + this.contentWidth = observableFromEvent(this.editor.onDidContentSizeChange, () => this.editor.getContentWidth()); + this._widgetCounter = 0; + this.openedPeekWidgets = observableValue(this, 0); this._register(this.editor.onBeginUpdate(() => this._beginUpdate())); this._register(this.editor.onEndUpdate(() => this._endUpdate())); @@ -149,93 +227,46 @@ export class ObservableCodeEditor extends Disposable { } } - private readonly _model = observableValue(this, this.editor.getModel()); - public readonly model: IObservable = this._model; + private readonly _model; + public readonly model: IObservable; - public readonly isReadonly = observableFromEvent(this, this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.readOnly)); + public readonly isReadonly; - private readonly _versionId = observableValueOpts({ owner: this, lazy: true }, this.editor.getModel()?.getVersionId() ?? null); - public readonly versionId: IObservableWithChange = this._versionId; + private readonly _versionId; + public readonly versionId: IObservableWithChange; - private readonly _selections = observableValueOpts( - { owner: this, equalsFn: equalsIfDefined(itemsEquals(Selection.selectionsEqual)), lazy: true }, - this.editor.getSelections() ?? null - ); - public readonly selections: IObservableWithChange = this._selections; + private readonly _selections; + public readonly selections: IObservableWithChange; - public readonly positions = derivedOpts( - { owner: this, equalsFn: equalsIfDefined(itemsEquals(Position.equals)) }, - reader => this.selections.read(reader)?.map(s => s.getStartPosition()) ?? null - ); + public readonly positions; - public readonly isFocused = observableFromEvent(this, e => { - const d1 = this.editor.onDidFocusEditorWidget(e); - const d2 = this.editor.onDidBlurEditorWidget(e); - return { - dispose() { - d1.dispose(); - d2.dispose(); - } - }; - }, () => this.editor.hasWidgetFocus()); + public readonly isFocused; - public readonly isTextFocused = observableFromEvent(this, e => { - const d1 = this.editor.onDidFocusEditorText(e); - const d2 = this.editor.onDidBlurEditorText(e); - return { - dispose() { - d1.dispose(); - d2.dispose(); - } - }; - }, () => this.editor.hasTextFocus()); + public readonly isTextFocused; - public readonly inComposition = observableFromEvent(this, e => { - const d1 = this.editor.onDidCompositionStart(() => { - e(undefined); - }); - const d2 = this.editor.onDidCompositionEnd(() => { - e(undefined); - }); - return { - dispose() { - d1.dispose(); - d2.dispose(); - } - }; - }, () => this.editor.inComposition); + public readonly inComposition; - public readonly value = derivedWithSetter(this, - reader => { this.versionId.read(reader); return this.model.read(reader)?.getValue() ?? ''; }, - (value, tx) => { - const model = this.model.get(); - if (model !== null) { - if (value !== model.getValue()) { - model.setValue(value); - } - } - } - ); - public readonly valueIsEmpty = derived(this, reader => { this.versionId.read(reader); return this.editor.getModel()?.getValueLength() === 0; }); - public readonly cursorSelection = derivedOpts({ owner: this, equalsFn: equalsIfDefined(Selection.selectionsEqual) }, reader => this.selections.read(reader)?.[0] ?? null); - public readonly cursorPosition = derivedOpts({ owner: this, equalsFn: Position.equals }, reader => this.selections.read(reader)?.[0]?.getPosition() ?? null); - public readonly cursorLineNumber = derived(this, reader => this.cursorPosition.read(reader)?.lineNumber ?? null); + public readonly value; + public readonly valueIsEmpty; + public readonly cursorSelection; + public readonly cursorPosition; + public readonly cursorLineNumber; - public readonly onDidType = observableSignal(this); - public readonly onDidPaste = observableSignal(this); + public readonly onDidType; + public readonly onDidPaste; - public readonly scrollTop = observableFromEvent(this.editor.onDidScrollChange, () => this.editor.getScrollTop()); - public readonly scrollLeft = observableFromEvent(this.editor.onDidScrollChange, () => this.editor.getScrollLeft()); + public readonly scrollTop; + public readonly scrollLeft; - public readonly layoutInfo = observableFromEvent(this.editor.onDidLayoutChange, () => this.editor.getLayoutInfo()); - public readonly layoutInfoContentLeft = this.layoutInfo.map(l => l.contentLeft); - public readonly layoutInfoDecorationsLeft = this.layoutInfo.map(l => l.decorationsLeft); - public readonly layoutInfoWidth = this.layoutInfo.map(l => l.width); - public readonly layoutInfoMinimap = this.layoutInfo.map(l => l.minimap); - public readonly layoutInfoVerticalScrollbarWidth = this.layoutInfo.map(l => l.verticalScrollbarWidth); + public readonly layoutInfo; + public readonly layoutInfoContentLeft; + public readonly layoutInfoDecorationsLeft; + public readonly layoutInfoWidth; + public readonly layoutInfoMinimap; + public readonly layoutInfoVerticalScrollbarWidth; - public readonly contentWidth = observableFromEvent(this.editor.onDidContentSizeChange, () => this.editor.getContentWidth()); + public readonly contentWidth; public getOption(id: T): IObservable> { return observableFromEvent(this, cb => this.editor.onDidChangeConfiguration(e => { @@ -258,7 +289,7 @@ export class ObservableCodeEditor extends Disposable { return d; } - private _widgetCounter = 0; + private _widgetCounter; public createOverlayWidget(widget: IObservableOverlayWidget): IDisposable { const overlayWidgetId = 'observableOverlayWidget' + (this._widgetCounter++); @@ -354,7 +385,7 @@ export class ObservableCodeEditor extends Disposable { return result; } - public readonly openedPeekWidgets = observableValue(this, 0); + public readonly openedPeekWidgets; isTargetHovered(predicate: (target: IEditorMouseEvent) => boolean, store: DisposableStore): IObservable { const isHovered = observableValue('isInjectedTextHovered', false); diff --git a/src/vs/editor/browser/view/viewLayer.ts b/src/vs/editor/browser/view/viewLayer.ts index 1b64760a7f4..3f1b0905954 100644 --- a/src/vs/editor/browser/view/viewLayer.ts +++ b/src/vs/editor/browser/view/viewLayer.ts @@ -252,13 +252,15 @@ export class RenderedLinesCollection { export class VisibleLinesCollection { - public readonly domNode: FastDomNode = this._createDomNode(); - private readonly _linesCollection: RenderedLinesCollection = new RenderedLinesCollection(this._lineFactory); + public readonly domNode: FastDomNode; + private readonly _linesCollection: RenderedLinesCollection; constructor( private readonly _viewContext: ViewContext, private readonly _lineFactory: ILineFactory, ) { + this.domNode = this._createDomNode(); + this._linesCollection = new RenderedLinesCollection(this._lineFactory); } private _createDomNode(): FastDomNode { diff --git a/src/vs/editor/browser/viewParts/viewLines/viewLine.ts b/src/vs/editor/browser/viewParts/viewLines/viewLine.ts index 77860593a2f..0b97fd500c9 100644 --- a/src/vs/editor/browser/viewParts/viewLines/viewLine.ts +++ b/src/vs/editor/browser/viewParts/viewLines/viewLine.ts @@ -19,7 +19,7 @@ import { EditorFontLigatures } from '../../../common/config/editorOptions.js'; import { DomReadingContext } from './domReadingContext.js'; import type { ViewLineOptions } from './viewLineOptions.js'; import { ViewGpuContext } from '../../gpu/viewGpuContext.js'; -import { OffsetRange } from '../../../common/core/offsetRange.js'; +import { OffsetRange } from '../../../common/core/ranges/offsetRange.js'; const canUseFastRenderedViewLine = (function () { if (platform.isNative) { diff --git a/src/vs/editor/browser/viewParts/whitespace/whitespace.ts b/src/vs/editor/browser/viewParts/whitespace/whitespace.ts index a9a9d372139..1cbc649f674 100644 --- a/src/vs/editor/browser/viewParts/whitespace/whitespace.ts +++ b/src/vs/editor/browser/viewParts/whitespace/whitespace.ts @@ -16,7 +16,7 @@ import * as strings from '../../../../base/common/strings.js'; import { CharCode } from '../../../../base/common/charCode.js'; import { Position } from '../../../common/core/position.js'; import { editorWhitespaces } from '../../../common/core/editorColorRegistry.js'; -import { OffsetRange } from '../../../common/core/offsetRange.js'; +import { OffsetRange } from '../../../common/core/ranges/offsetRange.js'; /** * The whitespace overlay will visual certain whitespace depending on the diff --git a/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts index 0ea325c3b1b..94504b00916 100644 --- a/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts @@ -597,12 +597,15 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE return CodeEditorWidget._getVerticalOffsetAfterPosition(this._modelData, lineNumber, maxCol, includeViewZones); } - public getLineHeightForLineNumber(lineNumber: number): number { + public getLineHeightForPosition(position: IPosition): number { if (!this._modelData) { return -1; } - const viewPosition = this._modelData.viewModel.coordinatesConverter.convertModelPositionToViewPosition(new Position(lineNumber, 1)); - return this._modelData.viewModel.viewLayout.getLineHeightForLineNumber(viewPosition.lineNumber); + const viewModel = this._modelData.viewModel; + if (viewModel.coordinatesConverter.modelPositionIsVisible(Position.lift(position))) { + return viewModel.viewLayout.getLineHeightForLineNumber(position.lineNumber); + } + return 0; } public setHiddenAreas(ranges: IRange[], source?: unknown, forceUpdate?: boolean): void { @@ -1609,7 +1612,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE const top = CodeEditorWidget._getVerticalOffsetForPosition(this._modelData, position.lineNumber, position.column) - this.getScrollTop(); const left = this._modelData.view.getOffsetForColumn(position.lineNumber, position.column) + layoutInfo.glyphMarginWidth + layoutInfo.lineNumbersWidth + layoutInfo.decorationsWidth - this.getScrollLeft(); - const height = this.getLineHeightForLineNumber(position.lineNumber); + const height = this.getLineHeightForPosition(position); return { top: top, left: left, diff --git a/src/vs/editor/browser/widget/diffEditor/components/accessibleDiffViewer.ts b/src/vs/editor/browser/widget/diffEditor/components/accessibleDiffViewer.ts index d6ea2d268bc..20d84267704 100644 --- a/src/vs/editor/browser/widget/diffEditor/components/accessibleDiffViewer.ts +++ b/src/vs/editor/browser/widget/diffEditor/components/accessibleDiffViewer.ts @@ -11,13 +11,13 @@ import { forEachAdjacent, groupAdjacentBy } from '../../../../../base/common/arr import { Codicon } from '../../../../../base/common/codicons.js'; import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; import { Disposable, DisposableStore, toDisposable } from '../../../../../base/common/lifecycle.js'; -import { IObservable, ITransaction, autorun, autorunWithStore, derived, derivedWithStore, observableValue, subtransaction, transaction } from '../../../../../base/common/observable.js'; +import { IObservable, ITransaction, autorun, autorunWithStore, derived, observableValue, subtransaction, transaction } from '../../../../../base/common/observable.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { applyFontInfo } from '../../../config/domFontInfo.js'; import { applyStyle } from '../utils.js'; import { EditorFontLigatures, EditorOption, IComputedEditorOptions } from '../../../../common/config/editorOptions.js'; import { LineRange } from '../../../../common/core/ranges/lineRange.js'; -import { OffsetRange } from '../../../../common/core/offsetRange.js'; +import { OffsetRange } from '../../../../common/core/ranges/offsetRange.js'; import { Position } from '../../../../common/core/position.js'; import { Range } from '../../../../common/core/range.js'; import { DetailedLineRangeMapping, LineRangeMapping } from '../../../../common/diff/rangeMapping.js'; @@ -77,14 +77,14 @@ export class AccessibleDiffViewer extends Disposable { super(); } - private readonly _state = derivedWithStore(this, (reader, store) => { + private readonly _state = derived(this, (reader) => { const visible = this._visible.read(reader); this._parentNode.style.visibility = visible ? 'visible' : 'hidden'; if (!visible) { return null; } - const model = store.add(this._instantiationService.createInstance(ViewModel, this._diffs, this._models, this._setVisible, this._canClose)); - const view = store.add(this._instantiationService.createInstance(View, this._parentNode, model, this._width, this._height, this._models)); + const model = reader.store.add(this._instantiationService.createInstance(ViewModel, this._diffs, this._models, this._setVisible, this._canClose)); + const view = reader.store.add(this._instantiationService.createInstance(View, this._parentNode, model, this._width, this._height, this._models)); return { model, view, }; }).recomputeInitiallyAndOnChange(this._store); diff --git a/src/vs/editor/browser/widget/diffEditor/components/diffEditorEditors.ts b/src/vs/editor/browser/widget/diffEditor/components/diffEditorEditors.ts index 3dfb032516c..58fa085504c 100644 --- a/src/vs/editor/browser/widget/diffEditor/components/diffEditorEditors.ts +++ b/src/vs/editor/browser/widget/diffEditor/components/diffEditorEditors.ts @@ -22,29 +22,29 @@ import { DiffEditorOptions } from '../diffEditorOptions.js'; import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; export class DiffEditorEditors extends Disposable { - public readonly original = this._register(this._createLeftHandSideEditor(this._options.editorOptions.get(), this._argCodeEditorWidgetOptions.originalEditor || {})); - public readonly modified = this._register(this._createRightHandSideEditor(this._options.editorOptions.get(), this._argCodeEditorWidgetOptions.modifiedEditor || {})); + public readonly original; + public readonly modified; - private readonly _onDidContentSizeChange = this._register(new Emitter()); + private readonly _onDidContentSizeChange; public get onDidContentSizeChange() { return this._onDidContentSizeChange.event; } - public readonly modifiedScrollTop = observableFromEvent(this, this.modified.onDidScrollChange, () => /** @description modified.getScrollTop */ this.modified.getScrollTop()); - public readonly modifiedScrollHeight = observableFromEvent(this, this.modified.onDidScrollChange, () => /** @description modified.getScrollHeight */ this.modified.getScrollHeight()); + public readonly modifiedScrollTop; + public readonly modifiedScrollHeight; - public readonly modifiedObs = observableCodeEditor(this.modified); - public readonly originalObs = observableCodeEditor(this.original); + public readonly modifiedObs; + public readonly originalObs; - public readonly modifiedModel = this.modifiedObs.model; + public readonly modifiedModel; - public readonly modifiedSelections = observableFromEvent(this, this.modified.onDidChangeCursorSelection, () => this.modified.getSelections() ?? []); - public readonly modifiedCursor = derivedOpts({ owner: this, equalsFn: Position.equals }, reader => this.modifiedSelections.read(reader)[0]?.getPosition() ?? new Position(1, 1)); + public readonly modifiedSelections; + public readonly modifiedCursor; - public readonly originalCursor = observableFromEvent(this, this.original.onDidChangeCursorPosition, () => this.original.getPosition() ?? new Position(1, 1)); + public readonly originalCursor; - public readonly isOriginalFocused = observableCodeEditor(this.original).isFocused; - public readonly isModifiedFocused = observableCodeEditor(this.modified).isFocused; + public readonly isOriginalFocused; + public readonly isModifiedFocused; - public readonly isFocused = derived(this, reader => this.isOriginalFocused.read(reader) || this.isModifiedFocused.read(reader)); + public readonly isFocused; constructor( private readonly originalEditorElement: HTMLElement, @@ -57,6 +57,20 @@ export class DiffEditorEditors extends Disposable { @IKeybindingService private readonly _keybindingService: IKeybindingService ) { super(); + this.original = this._register(this._createLeftHandSideEditor(this._options.editorOptions.get(), this._argCodeEditorWidgetOptions.originalEditor || {})); + this.modified = this._register(this._createRightHandSideEditor(this._options.editorOptions.get(), this._argCodeEditorWidgetOptions.modifiedEditor || {})); + this._onDidContentSizeChange = this._register(new Emitter()); + this.modifiedScrollTop = observableFromEvent(this, this.modified.onDidScrollChange, () => /** @description modified.getScrollTop */ this.modified.getScrollTop()); + this.modifiedScrollHeight = observableFromEvent(this, this.modified.onDidScrollChange, () => /** @description modified.getScrollHeight */ this.modified.getScrollHeight()); + this.modifiedObs = observableCodeEditor(this.modified); + this.originalObs = observableCodeEditor(this.original); + this.modifiedModel = this.modifiedObs.model; + this.modifiedSelections = observableFromEvent(this, this.modified.onDidChangeCursorSelection, () => this.modified.getSelections() ?? []); + this.modifiedCursor = derivedOpts({ owner: this, equalsFn: Position.equals }, reader => this.modifiedSelections.read(reader)[0]?.getPosition() ?? new Position(1, 1)); + this.originalCursor = observableFromEvent(this, this.original.onDidChangeCursorPosition, () => this.original.getPosition() ?? new Position(1, 1)); + this.isOriginalFocused = observableCodeEditor(this.original).isFocused; + this.isModifiedFocused = observableCodeEditor(this.modified).isFocused; + this.isFocused = derived(this, reader => this.isOriginalFocused.read(reader) || this.isModifiedFocused.read(reader)); this._argCodeEditorWidgetOptions = null as any; @@ -169,6 +183,7 @@ export class DiffEditorEditors extends Disposable { }; clonedOptions.inDiffEditor = true; clonedOptions.automaticLayout = false; + clonedOptions.allowVariableLineHeights = false; // Clone scrollbar options before changing them clonedOptions.scrollbar = { ...(clonedOptions.scrollbar || {}) }; diff --git a/src/vs/editor/browser/widget/diffEditor/components/diffEditorSash.ts b/src/vs/editor/browser/widget/diffEditor/components/diffEditorSash.ts index 468e6323c7f..8b6866ca458 100644 --- a/src/vs/editor/browser/widget/diffEditor/components/diffEditorSash.ts +++ b/src/vs/editor/browser/widget/diffEditor/components/diffEditorSash.ts @@ -50,13 +50,9 @@ export class SashLayout { } export class DiffEditorSash extends Disposable { - private readonly _sash = this._register(new Sash(this._domNode, { - getVerticalSashTop: (_sash: Sash): number => 0, - getVerticalSashLeft: (_sash: Sash): number => this.sashLeft.get(), - getVerticalSashHeight: (_sash: Sash): number => this._dimensions.height.get(), - }, { orientation: Orientation.VERTICAL })); + private readonly _sash; - private _startSashPosition: number | undefined = undefined; + private _startSashPosition: number | undefined; constructor( private readonly _domNode: HTMLElement, @@ -67,6 +63,12 @@ export class DiffEditorSash extends Disposable { private readonly _resetSash: () => void, ) { super(); + this._sash = this._register(new Sash(this._domNode, { + getVerticalSashTop: (_sash: Sash): number => 0, + getVerticalSashLeft: (_sash: Sash): number => this.sashLeft.get(), + getVerticalSashHeight: (_sash: Sash): number => this._dimensions.height.get(), + }, { orientation: Orientation.VERTICAL })); + this._startSashPosition = undefined; this._register(this._sash.onDidStart(() => { this._startSashPosition = this.sashLeft.get(); diff --git a/src/vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/diffEditorViewZones.ts b/src/vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/diffEditorViewZones.ts index b8ea4caae2e..ce4565f526f 100644 --- a/src/vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/diffEditorViewZones.ts +++ b/src/vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/diffEditorViewZones.ts @@ -8,7 +8,7 @@ import { ArrayQueue } from '../../../../../../base/common/arrays.js'; import { RunOnceScheduler } from '../../../../../../base/common/async.js'; import { Codicon } from '../../../../../../base/common/codicons.js'; import { Disposable, DisposableStore } from '../../../../../../base/common/lifecycle.js'; -import { IObservable, autorun, derived, derivedWithStore, observableFromEvent, observableValue } from '../../../../../../base/common/observable.js'; +import { IObservable, autorun, derived, observableFromEvent, observableValue } from '../../../../../../base/common/observable.js'; import { ThemeIcon } from '../../../../../../base/common/themables.js'; import { assertIsDefined } from '../../../../../../base/common/types.js'; import { applyFontInfo } from '../../../../config/domFontInfo.js'; @@ -40,15 +40,15 @@ import { Range } from '../../../../../common/core/range.js'; * Make sure to add the view zones! */ export class DiffEditorViewZones extends Disposable { - private readonly _originalTopPadding = observableValue(this, 0); + private readonly _originalTopPadding; private readonly _originalScrollTop: IObservable; - private readonly _originalScrollOffset = observableValue(this, 0); - private readonly _originalScrollOffsetAnimated = animatedObservable(this._targetWindow, this._originalScrollOffset, this._store); + private readonly _originalScrollOffset; + private readonly _originalScrollOffsetAnimated; - private readonly _modifiedTopPadding = observableValue(this, 0); + private readonly _modifiedTopPadding; private readonly _modifiedScrollTop: IObservable; - private readonly _modifiedScrollOffset = observableValue(this, 0); - private readonly _modifiedScrollOffsetAnimated = animatedObservable(this._targetWindow, this._modifiedScrollOffset, this._store); + private readonly _modifiedScrollOffset; + private readonly _modifiedScrollOffsetAnimated; public readonly viewZones: IObservable<{ orig: IObservableViewZone[]; mod: IObservableViewZone[] }>; @@ -65,6 +65,12 @@ export class DiffEditorViewZones extends Disposable { @IContextMenuService private readonly _contextMenuService: IContextMenuService, ) { super(); + this._originalTopPadding = observableValue(this, 0); + this._originalScrollOffset = observableValue(this, 0); + this._originalScrollOffsetAnimated = animatedObservable(this._targetWindow, this._originalScrollOffset, this._store); + this._modifiedTopPadding = observableValue(this, 0); + this._modifiedScrollOffset = observableValue(this, 0); + this._modifiedScrollOffsetAnimated = animatedObservable(this._targetWindow, this._modifiedScrollOffset, this._store); const state = observableValue('invalidateAlignmentsState', 0); @@ -127,7 +133,7 @@ export class DiffEditorViewZones extends Disposable { } const alignmentViewZonesDisposables = this._register(new DisposableStore()); - this.viewZones = derivedWithStore<{ orig: IObservableViewZone[]; mod: IObservableViewZone[] }>(this, (reader, store) => { + this.viewZones = derived<{ orig: IObservableViewZone[]; mod: IObservableViewZone[] }>(this, (reader) => { alignmentViewZonesDisposables.clear(); const alignmentsVal = alignments.read(reader) || []; @@ -304,8 +310,8 @@ export class DiffEditorViewZones extends Disposable { function createViewZoneMarginArrow(): HTMLElement { const arrow = document.createElement('div'); arrow.className = 'arrow-revert-change ' + ThemeIcon.asClassName(Codicon.arrowRight); - store.add(addDisposableListener(arrow, 'mousedown', e => e.stopPropagation())); - store.add(addDisposableListener(arrow, 'click', e => { + reader.store.add(addDisposableListener(arrow, 'mousedown', e => e.stopPropagation())); + reader.store.add(addDisposableListener(arrow, 'click', e => { e.stopPropagation(); _diffEditorWidget.revert(a.diff!); })); diff --git a/src/vs/editor/browser/widget/diffEditor/diffEditorOptions.ts b/src/vs/editor/browser/widget/diffEditor/diffEditorOptions.ts index 3d2e483ca15..48ae36e0b90 100644 --- a/src/vs/editor/browser/widget/diffEditor/diffEditorOptions.ts +++ b/src/vs/editor/browser/widget/diffEditor/diffEditorOptions.ts @@ -17,71 +17,109 @@ export class DiffEditorOptions { public get editorOptions(): IObservableWithChange { return this._options; } - private readonly _diffEditorWidth = observableValue(this, 0); + private readonly _diffEditorWidth; - private readonly _screenReaderMode = observableFromEvent(this, this._accessibilityService.onDidChangeScreenReaderOptimized, () => this._accessibilityService.isScreenReaderOptimized()); + private readonly _screenReaderMode; constructor( options: Readonly, @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, ) { + this._diffEditorWidth = observableValue(this, 0); + this._screenReaderMode = observableFromEvent(this, this._accessibilityService.onDidChangeScreenReaderOptimized, () => this._accessibilityService.isScreenReaderOptimized()); + this.couldShowInlineViewBecauseOfSize = derived(this, reader => + this._options.read(reader).renderSideBySide && this._diffEditorWidth.read(reader) <= this._options.read(reader).renderSideBySideInlineBreakpoint + ); + this.renderOverviewRuler = derived(this, reader => this._options.read(reader).renderOverviewRuler); + this.renderSideBySide = derived(this, reader => { + if (this.compactMode.read(reader)) { + if (this.shouldRenderInlineViewInSmartMode.read(reader)) { + return false; + } + } + + return this._options.read(reader).renderSideBySide + && !(this._options.read(reader).useInlineViewWhenSpaceIsLimited && this.couldShowInlineViewBecauseOfSize.read(reader) && !this._screenReaderMode.read(reader)); + }); + this.readOnly = derived(this, reader => this._options.read(reader).readOnly); + this.shouldRenderOldRevertArrows = derived(this, reader => { + if (!this._options.read(reader).renderMarginRevertIcon) { return false; } + if (!this.renderSideBySide.read(reader)) { return false; } + if (this.readOnly.read(reader)) { return false; } + if (this.shouldRenderGutterMenu.read(reader)) { return false; } + return true; + }); + this.shouldRenderGutterMenu = derived(this, reader => this._options.read(reader).renderGutterMenu); + this.renderIndicators = derived(this, reader => this._options.read(reader).renderIndicators); + this.enableSplitViewResizing = derived(this, reader => this._options.read(reader).enableSplitViewResizing); + this.splitViewDefaultRatio = derived(this, reader => this._options.read(reader).splitViewDefaultRatio); + this.ignoreTrimWhitespace = derived(this, reader => this._options.read(reader).ignoreTrimWhitespace); + this.maxComputationTimeMs = derived(this, reader => this._options.read(reader).maxComputationTime); + this.showMoves = derived(this, reader => this._options.read(reader).experimental.showMoves! && this.renderSideBySide.read(reader)); + this.isInEmbeddedEditor = derived(this, reader => this._options.read(reader).isInEmbeddedEditor); + this.diffWordWrap = derived(this, reader => this._options.read(reader).diffWordWrap); + this.originalEditable = derived(this, reader => this._options.read(reader).originalEditable); + this.diffCodeLens = derived(this, reader => this._options.read(reader).diffCodeLens); + this.accessibilityVerbose = derived(this, reader => this._options.read(reader).accessibilityVerbose); + this.diffAlgorithm = derived(this, reader => this._options.read(reader).diffAlgorithm); + this.showEmptyDecorations = derived(this, reader => this._options.read(reader).experimental.showEmptyDecorations!); + this.onlyShowAccessibleDiffViewer = derived(this, reader => this._options.read(reader).onlyShowAccessibleDiffViewer); + this.compactMode = derived(this, reader => this._options.read(reader).compactMode); + this.trueInlineDiffRenderingEnabled = derived(this, reader => + this._options.read(reader).experimental.useTrueInlineView! + ); + this.useTrueInlineDiffRendering = derived(this, reader => + !this.renderSideBySide.read(reader) && this.trueInlineDiffRenderingEnabled.read(reader) + ); + this.hideUnchangedRegions = derived(this, reader => this._options.read(reader).hideUnchangedRegions.enabled!); + this.hideUnchangedRegionsRevealLineCount = derived(this, reader => this._options.read(reader).hideUnchangedRegions.revealLineCount!); + this.hideUnchangedRegionsContextLineCount = derived(this, reader => this._options.read(reader).hideUnchangedRegions.contextLineCount!); + this.hideUnchangedRegionsMinimumLineCount = derived(this, reader => this._options.read(reader).hideUnchangedRegions.minimumLineCount!); + this._model = observableValue(this, undefined); + this.shouldRenderInlineViewInSmartMode = this._model + .map(this, model => derivedConstOnceDefined(this, reader => { + const diffs = model?.diff.read(reader); + return diffs ? isSimpleDiff(diffs, this.trueInlineDiffRenderingEnabled.read(reader)) : undefined; + })) + .flatten() + .map(this, v => !!v); + this.inlineViewHideOriginalLineNumbers = this.compactMode; const optionsCopy = { ...options, ...validateDiffEditorOptions(options, diffEditorDefaultOptions) }; this._options = observableValue(this, optionsCopy); } - public readonly couldShowInlineViewBecauseOfSize = derived(this, reader => - this._options.read(reader).renderSideBySide && this._diffEditorWidth.read(reader) <= this._options.read(reader).renderSideBySideInlineBreakpoint - ); + public readonly couldShowInlineViewBecauseOfSize; - public readonly renderOverviewRuler = derived(this, reader => this._options.read(reader).renderOverviewRuler); - public readonly renderSideBySide = derived(this, reader => { - if (this.compactMode.read(reader)) { - if (this.shouldRenderInlineViewInSmartMode.read(reader)) { - return false; - } - } + public readonly renderOverviewRuler; + public readonly renderSideBySide; + public readonly readOnly; - return this._options.read(reader).renderSideBySide - && !(this._options.read(reader).useInlineViewWhenSpaceIsLimited && this.couldShowInlineViewBecauseOfSize.read(reader) && !this._screenReaderMode.read(reader)); - }); - public readonly readOnly = derived(this, reader => this._options.read(reader).readOnly); + public readonly shouldRenderOldRevertArrows; - public readonly shouldRenderOldRevertArrows = derived(this, reader => { - if (!this._options.read(reader).renderMarginRevertIcon) { return false; } - if (!this.renderSideBySide.read(reader)) { return false; } - if (this.readOnly.read(reader)) { return false; } - if (this.shouldRenderGutterMenu.read(reader)) { return false; } - return true; - }); + public readonly shouldRenderGutterMenu; + public readonly renderIndicators; + public readonly enableSplitViewResizing; + public readonly splitViewDefaultRatio; + public readonly ignoreTrimWhitespace; + public readonly maxComputationTimeMs; + public readonly showMoves; + public readonly isInEmbeddedEditor; + public readonly diffWordWrap; + public readonly originalEditable; + public readonly diffCodeLens; + public readonly accessibilityVerbose; + public readonly diffAlgorithm; + public readonly showEmptyDecorations; + public readonly onlyShowAccessibleDiffViewer; + public readonly compactMode; + private readonly trueInlineDiffRenderingEnabled: IObservable; - public readonly shouldRenderGutterMenu = derived(this, reader => this._options.read(reader).renderGutterMenu); - public readonly renderIndicators = derived(this, reader => this._options.read(reader).renderIndicators); - public readonly enableSplitViewResizing = derived(this, reader => this._options.read(reader).enableSplitViewResizing); - public readonly splitViewDefaultRatio = derived(this, reader => this._options.read(reader).splitViewDefaultRatio); - public readonly ignoreTrimWhitespace = derived(this, reader => this._options.read(reader).ignoreTrimWhitespace); - public readonly maxComputationTimeMs = derived(this, reader => this._options.read(reader).maxComputationTime); - public readonly showMoves = derived(this, reader => this._options.read(reader).experimental.showMoves! && this.renderSideBySide.read(reader)); - public readonly isInEmbeddedEditor = derived(this, reader => this._options.read(reader).isInEmbeddedEditor); - public readonly diffWordWrap = derived(this, reader => this._options.read(reader).diffWordWrap); - public readonly originalEditable = derived(this, reader => this._options.read(reader).originalEditable); - public readonly diffCodeLens = derived(this, reader => this._options.read(reader).diffCodeLens); - public readonly accessibilityVerbose = derived(this, reader => this._options.read(reader).accessibilityVerbose); - public readonly diffAlgorithm = derived(this, reader => this._options.read(reader).diffAlgorithm); - public readonly showEmptyDecorations = derived(this, reader => this._options.read(reader).experimental.showEmptyDecorations!); - public readonly onlyShowAccessibleDiffViewer = derived(this, reader => this._options.read(reader).onlyShowAccessibleDiffViewer); - public readonly compactMode = derived(this, reader => this._options.read(reader).compactMode); - private readonly trueInlineDiffRenderingEnabled: IObservable = derived(this, reader => - this._options.read(reader).experimental.useTrueInlineView! - ); + public readonly useTrueInlineDiffRendering: IObservable; - public readonly useTrueInlineDiffRendering: IObservable = derived(this, reader => - !this.renderSideBySide.read(reader) && this.trueInlineDiffRenderingEnabled.read(reader) - ); - - public readonly hideUnchangedRegions = derived(this, reader => this._options.read(reader).hideUnchangedRegions.enabled!); - public readonly hideUnchangedRegionsRevealLineCount = derived(this, reader => this._options.read(reader).hideUnchangedRegions.revealLineCount!); - public readonly hideUnchangedRegionsContextLineCount = derived(this, reader => this._options.read(reader).hideUnchangedRegions.contextLineCount!); - public readonly hideUnchangedRegionsMinimumLineCount = derived(this, reader => this._options.read(reader).hideUnchangedRegions.minimumLineCount!); + public readonly hideUnchangedRegions; + public readonly hideUnchangedRegionsRevealLineCount; + public readonly hideUnchangedRegionsContextLineCount; + public readonly hideUnchangedRegionsMinimumLineCount; public updateOptions(changedOptions: IDiffEditorOptions): void { const newDiffEditorOptions = validateDiffEditorOptions(changedOptions, this._options.get()); @@ -93,21 +131,15 @@ export class DiffEditorOptions { this._diffEditorWidth.set(width, undefined); } - private readonly _model = observableValue(this, undefined); + private readonly _model; public setModel(model: DiffEditorViewModel | undefined) { this._model.set(model, undefined); } - private readonly shouldRenderInlineViewInSmartMode = this._model - .map(this, model => derivedConstOnceDefined(this, reader => { - const diffs = model?.diff.read(reader); - return diffs ? isSimpleDiff(diffs, this.trueInlineDiffRenderingEnabled.read(reader)) : undefined; - })) - .flatten() - .map(this, v => !!v); + private readonly shouldRenderInlineViewInSmartMode; - public readonly inlineViewHideOriginalLineNumbers = this.compactMode; + public readonly inlineViewHideOriginalLineNumbers; } function isSimpleDiff(diff: DiffState, supportsTrueDiffRendering: boolean): boolean { diff --git a/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts b/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts index eb130dd134b..9e19a590a16 100644 --- a/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts +++ b/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts @@ -56,40 +56,30 @@ export interface IDiffCodeEditorWidgetOptions { export class DiffEditorWidget extends DelegatingEditor implements IDiffEditor { public static ENTIRE_DIFF_OVERVIEW_WIDTH = OverviewRulerFeature.ENTIRE_DIFF_OVERVIEW_WIDTH; - private readonly elements = h('div.monaco-diff-editor.side-by-side', { style: { position: 'relative', height: '100%' } }, [ - h('div.editor.original@original', { style: { position: 'absolute', height: '100%', } }), - h('div.editor.modified@modified', { style: { position: 'absolute', height: '100%', } }), - h('div.accessibleDiffViewer@accessibleDiffViewer', { style: { position: 'absolute', height: '100%' } }), - ]); - private readonly _diffModelSrc = this._register(disposableObservableValue | undefined>(this, undefined)); - private readonly _diffModel = derived(this, reader => this._diffModelSrc.read(reader)?.object); - public readonly onDidChangeModel = Event.fromObservableLight(this._diffModel); + private readonly elements; + private readonly _diffModelSrc; + private readonly _diffModel; + public readonly onDidChangeModel; public get onDidContentSizeChange() { return this._editors.onDidContentSizeChange; } - private readonly _contextKeyService = this._register(this._parentContextKeyService.createScoped(this._domElement)); - private readonly _instantiationService = this._register(this._parentInstantiationService.createChild( - new ServiceCollection([IContextKeyService, this._contextKeyService]) - )); + private readonly _contextKeyService; + private readonly _instantiationService; private readonly _rootSizeObserver: ObservableElementSizeObserver; private readonly _sashLayout: SashLayout; private readonly _sash: IObservable; - private readonly _boundarySashes = observableValue(this, undefined); + private readonly _boundarySashes; - private _accessibleDiffViewerShouldBeVisible = observableValue(this, false); - private _accessibleDiffViewerVisible = derived(this, reader => - this._options.onlyShowAccessibleDiffViewer.read(reader) - ? true - : this._accessibleDiffViewerShouldBeVisible.read(reader) - ); + private _accessibleDiffViewerShouldBeVisible; + private _accessibleDiffViewerVisible; private readonly _accessibleDiffViewer: IObservable; private readonly _options: DiffEditorOptions; private readonly _editors: DiffEditorEditors; private readonly _overviewRulerPart: IObservable; - private readonly _movedBlocksLinesPart = observableValue(this, undefined); + private readonly _movedBlocksLinesPart; private readonly _gutter: IObservable; @@ -106,6 +96,89 @@ export class DiffEditorWidget extends DelegatingEditor implements IDiffEditor { @IEditorProgressService private readonly _editorProgressService: IEditorProgressService, ) { super(); + this.elements = h('div.monaco-diff-editor.side-by-side', { style: { position: 'relative', height: '100%' } }, [ + h('div.editor.original@original', { style: { position: 'absolute', height: '100%', } }), + h('div.editor.modified@modified', { style: { position: 'absolute', height: '100%', } }), + h('div.accessibleDiffViewer@accessibleDiffViewer', { style: { position: 'absolute', height: '100%' } }), + ]); + this._diffModelSrc = this._register(disposableObservableValue | undefined>(this, undefined)); + this._diffModel = derived(this, reader => this._diffModelSrc.read(reader)?.object); + this.onDidChangeModel = Event.fromObservableLight(this._diffModel); + this._contextKeyService = this._register(this._parentContextKeyService.createScoped(this._domElement)); + this._instantiationService = this._register(this._parentInstantiationService.createChild( + new ServiceCollection([IContextKeyService, this._contextKeyService]) + )); + this._boundarySashes = observableValue(this, undefined); + this._accessibleDiffViewerShouldBeVisible = observableValue(this, false); + this._accessibleDiffViewerVisible = derived(this, reader => + this._options.onlyShowAccessibleDiffViewer.read(reader) + ? true + : this._accessibleDiffViewerShouldBeVisible.read(reader) + ); + this._movedBlocksLinesPart = observableValue(this, undefined); + this._layoutInfo = derived(this, reader => { + const fullWidth = this._rootSizeObserver.width.read(reader); + const fullHeight = this._rootSizeObserver.height.read(reader); + + if (this._rootSizeObserver.automaticLayout) { + this.elements.root.style.height = '100%'; + } else { + this.elements.root.style.height = fullHeight + 'px'; + } + + const sash = this._sash.read(reader); + + const gutter = this._gutter.read(reader); + const gutterWidth = gutter?.width.read(reader) ?? 0; + + const overviewRulerPartWidth = this._overviewRulerPart.read(reader)?.width ?? 0; + + let originalLeft: number, originalWidth: number, modifiedLeft: number, modifiedWidth: number, gutterLeft: number; + + const sideBySide = !!sash; + if (sideBySide) { + const sashLeft = sash.sashLeft.read(reader); + const movedBlocksLinesWidth = this._movedBlocksLinesPart.read(reader)?.width.read(reader) ?? 0; + + originalLeft = 0; + originalWidth = sashLeft - gutterWidth - movedBlocksLinesWidth; + + gutterLeft = sashLeft - gutterWidth; + + modifiedLeft = sashLeft; + modifiedWidth = fullWidth - modifiedLeft - overviewRulerPartWidth; + } else { + gutterLeft = 0; + + const shouldHideOriginalLineNumbers = this._options.inlineViewHideOriginalLineNumbers.read(reader); + originalLeft = gutterWidth; + if (shouldHideOriginalLineNumbers) { + originalWidth = 0; + } else { + originalWidth = Math.max(5, this._editors.originalObs.layoutInfoDecorationsLeft.read(reader)); + } + + modifiedLeft = gutterWidth + originalWidth; + modifiedWidth = fullWidth - modifiedLeft - overviewRulerPartWidth; + } + + this.elements.original.style.left = originalLeft + 'px'; + this.elements.original.style.width = originalWidth + 'px'; + this._editors.original.layout({ width: originalWidth, height: fullHeight }, true); + + gutter?.layout(gutterLeft); + + this.elements.modified.style.left = modifiedLeft + 'px'; + this.elements.modified.style.width = modifiedWidth + 'px'; + this._editors.modified.layout({ width: modifiedWidth, height: fullHeight }, true); + + return { + modifiedEditor: this._editors.modified.getLayoutInfo(), + originalEditor: this._editors.original.getLayoutInfo(), + }; + }); + this._diffValue = this._diffModel.map((m, r) => m?.diff.read(r)); + this.onDidUpdateDiff = Event.fromObservableLight(this._diffValue); codeEditorService.willCreateDiffEditor(); this._contextKeyService.createKey('isInDiffEditor', true); @@ -350,67 +423,7 @@ export class DiffEditorWidget extends DelegatingEditor implements IDiffEditor { return editor; } - private readonly _layoutInfo = derived(this, reader => { - const fullWidth = this._rootSizeObserver.width.read(reader); - const fullHeight = this._rootSizeObserver.height.read(reader); - - if (this._rootSizeObserver.automaticLayout) { - this.elements.root.style.height = '100%'; - } else { - this.elements.root.style.height = fullHeight + 'px'; - } - - const sash = this._sash.read(reader); - - const gutter = this._gutter.read(reader); - const gutterWidth = gutter?.width.read(reader) ?? 0; - - const overviewRulerPartWidth = this._overviewRulerPart.read(reader)?.width ?? 0; - - let originalLeft: number, originalWidth: number, modifiedLeft: number, modifiedWidth: number, gutterLeft: number; - - const sideBySide = !!sash; - if (sideBySide) { - const sashLeft = sash.sashLeft.read(reader); - const movedBlocksLinesWidth = this._movedBlocksLinesPart.read(reader)?.width.read(reader) ?? 0; - - originalLeft = 0; - originalWidth = sashLeft - gutterWidth - movedBlocksLinesWidth; - - gutterLeft = sashLeft - gutterWidth; - - modifiedLeft = sashLeft; - modifiedWidth = fullWidth - modifiedLeft - overviewRulerPartWidth; - } else { - gutterLeft = 0; - - const shouldHideOriginalLineNumbers = this._options.inlineViewHideOriginalLineNumbers.read(reader); - originalLeft = gutterWidth; - if (shouldHideOriginalLineNumbers) { - originalWidth = 0; - } else { - originalWidth = Math.max(5, this._editors.originalObs.layoutInfoDecorationsLeft.read(reader)); - } - - modifiedLeft = gutterWidth + originalWidth; - modifiedWidth = fullWidth - modifiedLeft - overviewRulerPartWidth; - } - - this.elements.original.style.left = originalLeft + 'px'; - this.elements.original.style.width = originalWidth + 'px'; - this._editors.original.layout({ width: originalWidth, height: fullHeight }, true); - - gutter?.layout(gutterLeft); - - this.elements.modified.style.left = modifiedLeft + 'px'; - this.elements.modified.style.width = modifiedWidth + 'px'; - this._editors.modified.layout({ width: modifiedWidth, height: fullHeight }, true); - - return { - modifiedEditor: this._editors.modified.getLayoutInfo(), - originalEditor: this._editors.original.getLayoutInfo(), - }; - }); + private readonly _layoutInfo; private _createDiffEditorContributions() { const contributions: IDiffEditorContributionDescription[] = EditorExtensionsRegistry.getDiffEditorContributions(); @@ -526,8 +539,8 @@ export class DiffEditorWidget extends DelegatingEditor implements IDiffEditor { this._boundarySashes.set(sashes, undefined); } - private readonly _diffValue = this._diffModel.map((m, r) => m?.diff.read(r)); - readonly onDidUpdateDiff: Event = Event.fromObservableLight(this._diffValue); + private readonly _diffValue; + readonly onDidUpdateDiff: Event; get ignoreTrimWhitespace(): boolean { return this._options.ignoreTrimWhitespace.get(); } diff --git a/src/vs/editor/browser/widget/diffEditor/features/gutterFeature.ts b/src/vs/editor/browser/widget/diffEditor/features/gutterFeature.ts index 50f2c512102..6033069a413 100644 --- a/src/vs/editor/browser/widget/diffEditor/features/gutterFeature.ts +++ b/src/vs/editor/browser/widget/diffEditor/features/gutterFeature.ts @@ -18,7 +18,7 @@ import { WorkbenchHoverDelegate } from '../../../../../platform/hover/browser/ho import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { EditorOption } from '../../../../common/config/editorOptions.js'; import { LineRange, LineRangeSet } from '../../../../common/core/ranges/lineRange.js'; -import { OffsetRange } from '../../../../common/core/offsetRange.js'; +import { OffsetRange } from '../../../../common/core/ranges/offsetRange.js'; import { Range } from '../../../../common/core/range.js'; import { TextEdit } from '../../../../common/core/edits/textEdit.js'; import { DetailedLineRangeMapping } from '../../../../common/diff/rangeMapping.js'; @@ -35,14 +35,14 @@ const emptyArr: never[] = []; const width = 35; export class DiffEditorGutter extends Disposable { - private readonly _menu = this._register(this._menuService.createMenu(MenuId.DiffEditorHunkToolbar, this._contextKeyService)); - private readonly _actions = observableFromEvent(this, this._menu.onDidChange, () => this._menu.getActions()); - private readonly _hasActions = this._actions.map(a => a.length > 0); - private readonly _showSash = derived(this, reader => this._options.renderSideBySide.read(reader) && this._hasActions.read(reader)); + private readonly _menu; + private readonly _actions; + private readonly _hasActions; + private readonly _showSash; - public readonly width = derived(this, reader => this._hasActions.read(reader) ? width : 0); + public readonly width; - private readonly elements = h('div.gutter@gutter', { style: { position: 'absolute', height: '100%', width: width + 'px' } }, []); + private readonly elements; constructor( diffEditorRoot: HTMLDivElement, @@ -56,6 +56,48 @@ export class DiffEditorGutter extends Disposable { @IMenuService private readonly _menuService: IMenuService, ) { super(); + this._menu = this._register(this._menuService.createMenu(MenuId.DiffEditorHunkToolbar, this._contextKeyService)); + this._actions = observableFromEvent(this, this._menu.onDidChange, () => this._menu.getActions()); + this._hasActions = this._actions.map(a => a.length > 0); + this._showSash = derived(this, reader => this._options.renderSideBySide.read(reader) && this._hasActions.read(reader)); + this.width = derived(this, reader => this._hasActions.read(reader) ? width : 0); + this.elements = h('div.gutter@gutter', { style: { position: 'absolute', height: '100%', width: width + 'px' } }, []); + this._currentDiff = derived(this, (reader) => { + const model = this._diffModel.read(reader); + if (!model) { + return undefined; + } + const mappings = model.diff.read(reader)?.mappings; + + const cursorPosition = this._editors.modifiedCursor.read(reader); + if (!cursorPosition) { return undefined; } + + return mappings?.find(m => m.lineRangeMapping.modified.contains(cursorPosition.lineNumber)); + }); + this._selectedDiffs = derived(this, (reader) => { + /** @description selectedDiffs */ + const model = this._diffModel.read(reader); + const diff = model?.diff.read(reader); + // Return `emptyArr` because it is a constant. [] is always a new array and would trigger a change. + if (!diff) { return emptyArr; } + + const selections = this._editors.modifiedSelections.read(reader); + if (selections.every(s => s.isEmpty())) { return emptyArr; } + + const selectedLineNumbers = new LineRangeSet(selections.map(s => LineRange.fromRangeInclusive(s))); + + const selectedMappings = diff.mappings.filter(m => + m.lineRangeMapping.innerChanges && selectedLineNumbers.intersects(m.lineRangeMapping.modified) + ); + const result = selectedMappings.map(mapping => ({ + mapping, + rangeMappings: mapping.lineRangeMapping.innerChanges!.filter( + c => selections.some(s => Range.areIntersecting(c.modifiedRange, s)) + ) + })); + if (result.length === 0 || result.every(r => r.rangeMappings.length === 0)) { return emptyArr; } + return result; + }); this._register(prependRemoveOnDispose(diffEditorRoot, this.elements.root)); @@ -138,43 +180,9 @@ export class DiffEditorGutter extends Disposable { return value; } - private readonly _currentDiff = derived(this, (reader) => { - const model = this._diffModel.read(reader); - if (!model) { - return undefined; - } - const mappings = model.diff.read(reader)?.mappings; + private readonly _currentDiff; - const cursorPosition = this._editors.modifiedCursor.read(reader); - if (!cursorPosition) { return undefined; } - - return mappings?.find(m => m.lineRangeMapping.modified.contains(cursorPosition.lineNumber)); - }); - - private readonly _selectedDiffs = derived(this, (reader) => { - /** @description selectedDiffs */ - const model = this._diffModel.read(reader); - const diff = model?.diff.read(reader); - // Return `emptyArr` because it is a constant. [] is always a new array and would trigger a change. - if (!diff) { return emptyArr; } - - const selections = this._editors.modifiedSelections.read(reader); - if (selections.every(s => s.isEmpty())) { return emptyArr; } - - const selectedLineNumbers = new LineRangeSet(selections.map(s => LineRange.fromRangeInclusive(s))); - - const selectedMappings = diff.mappings.filter(m => - m.lineRangeMapping.innerChanges && selectedLineNumbers.intersects(m.lineRangeMapping.modified) - ); - const result = selectedMappings.map(mapping => ({ - mapping, - rangeMappings: mapping.lineRangeMapping.innerChanges!.filter( - c => selections.some(s => Range.areIntersecting(c.modifiedRange, s)) - ) - })); - if (result.length === 0 || result.every(r => r.rangeMappings.length === 0)) { return emptyArr; } - return result; - }); + private readonly _selectedDiffs; layout(left: number) { this.elements.gutter.style.left = left + 'px'; @@ -197,15 +205,12 @@ class DiffGutterItem implements IGutterItemInfo { class DiffToolBar extends Disposable implements IGutterItemView { - private readonly _elements = h('div.gutterItem', { style: { height: '20px', width: '34px' } }, [ - h('div.background@background', {}, []), - h('div.buttons@buttons', {}, []), - ]); + private readonly _elements; - private readonly _showAlways = this._item.map(this, item => item.showAlways); - private readonly _menuId = this._item.map(this, item => item.menuId); + private readonly _showAlways; + private readonly _menuId; - private readonly _isSmall = observableValue(this, false); + private readonly _isSmall; constructor( private readonly _item: IObservable, @@ -214,6 +219,15 @@ class DiffToolBar extends Disposable implements IGutterItemView { @IInstantiationService instantiationService: IInstantiationService ) { super(); + this._elements = h('div.gutterItem', { style: { height: '20px', width: '34px' } }, [ + h('div.background@background', {}, []), + h('div.buttons@buttons', {}, []), + ]); + this._showAlways = this._item.map(this, item => item.showAlways); + this._menuId = this._item.map(this, item => item.menuId); + this._isSmall = observableValue(this, false); + this._lastItemRange = undefined; + this._lastViewRange = undefined; const hoverDelegate = this._register(instantiationService.createInstance( WorkbenchHoverDelegate, @@ -267,8 +281,8 @@ class DiffToolBar extends Disposable implements IGutterItemView { })); } - private _lastItemRange: OffsetRange | undefined = undefined; - private _lastViewRange: OffsetRange | undefined = undefined; + private _lastItemRange: OffsetRange | undefined; + private _lastViewRange: OffsetRange | undefined; layout(itemRange: OffsetRange, viewRange: OffsetRange): void { this._lastItemRange = itemRange; diff --git a/src/vs/editor/browser/widget/diffEditor/features/hideUnchangedRegionsFeature.ts b/src/vs/editor/browser/widget/diffEditor/features/hideUnchangedRegionsFeature.ts index a1c54c56381..0f829bfdd9e 100644 --- a/src/vs/editor/browser/widget/diffEditor/features/hideUnchangedRegionsFeature.ts +++ b/src/vs/editor/browser/widget/diffEditor/features/hideUnchangedRegionsFeature.ts @@ -8,7 +8,7 @@ import { renderIcon, renderLabelWithIcons } from '../../../../../base/browser/ui import { Codicon } from '../../../../../base/common/codicons.js'; import { MarkdownString } from '../../../../../base/common/htmlContent.js'; import { Disposable, IDisposable } from '../../../../../base/common/lifecycle.js'; -import { IObservable, IReader, autorun, derived, derivedDisposable, derivedWithStore, observableValue, transaction } from '../../../../../base/common/observable.js'; +import { IObservable, IReader, autorun, derived, derivedDisposable, observableValue, transaction } from '../../../../../base/common/observable.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { isDefined } from '../../../../../base/common/types.js'; import { localize } from '../../../../../nls.js'; @@ -95,7 +95,7 @@ export class HideUnchangedRegionsFeature extends Disposable { return regions; }); - this.viewZones = derivedWithStore(this, (reader, store) => { + this.viewZones = derived(this, (reader) => { /** @description view Zones */ const modifiedOutlineSource = this._modifiedOutlineSource.read(reader); if (!modifiedOutlineSource) { return { origViewZones: [], modViewZones: [] }; } @@ -122,7 +122,7 @@ export class HideUnchangedRegionsFeature extends Disposable { const d = derived(this, reader => /** @description hiddenOriginalRangeStart */ r.getHiddenOriginalRange(reader).startLineNumber - 1); const origVz = new PlaceholderViewZone(d, 12); origViewZones.push(origVz); - store.add(new CompactCollapsedCodeOverlayWidget( + reader.store.add(new CompactCollapsedCodeOverlayWidget( this._editors.original, origVz, r, @@ -133,7 +133,7 @@ export class HideUnchangedRegionsFeature extends Disposable { const d = derived(this, reader => /** @description hiddenModifiedRangeStart */ r.getHiddenModifiedRange(reader).startLineNumber - 1); const modViewZone = new PlaceholderViewZone(d, 12); modViewZones.push(modViewZone); - store.add(new CompactCollapsedCodeOverlayWidget( + reader.store.add(new CompactCollapsedCodeOverlayWidget( this._editors.modified, modViewZone, r, @@ -144,7 +144,7 @@ export class HideUnchangedRegionsFeature extends Disposable { const d = derived(this, reader => /** @description hiddenOriginalRangeStart */ r.getHiddenOriginalRange(reader).startLineNumber - 1); const origVz = new PlaceholderViewZone(d, 24); origViewZones.push(origVz); - store.add(new CollapsedCodeOverlayWidget( + reader.store.add(new CollapsedCodeOverlayWidget( this._editors.original, origVz, r, @@ -159,7 +159,7 @@ export class HideUnchangedRegionsFeature extends Disposable { const d = derived(this, reader => /** @description hiddenModifiedRangeStart */ r.getHiddenModifiedRange(reader).startLineNumber - 1); const modViewZone = new PlaceholderViewZone(d, 24); modViewZones.push(modViewZone); - store.add(new CollapsedCodeOverlayWidget( + reader.store.add(new CollapsedCodeOverlayWidget( this._editors.modified, modViewZone, r, diff --git a/src/vs/editor/browser/widget/diffEditor/features/movedBlocksLinesFeature.ts b/src/vs/editor/browser/widget/diffEditor/features/movedBlocksLinesFeature.ts index 49cb49b5796..04e0c61e024 100644 --- a/src/vs/editor/browser/widget/diffEditor/features/movedBlocksLinesFeature.ts +++ b/src/vs/editor/browser/widget/diffEditor/features/movedBlocksLinesFeature.ts @@ -10,7 +10,7 @@ import { booleanComparator, compareBy, numberComparator, tieBreakComparators } f import { findMaxIdx } from '../../../../../base/common/arraysFind.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { Disposable, toDisposable } from '../../../../../base/common/lifecycle.js'; -import { IObservable, autorun, autorunHandleChanges, autorunWithStore, constObservable, derived, derivedWithStore, observableFromEvent, observableSignalFromEvent, observableValue, recomputeInitiallyAndOnChange } from '../../../../../base/common/observable.js'; +import { IObservable, autorun, autorunHandleChanges, autorunWithStore, constObservable, derived, observableFromEvent, observableSignalFromEvent, observableValue, recomputeInitiallyAndOnChange } from '../../../../../base/common/observable.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { ICodeEditor } from '../../../editorBrowser.js'; import { DiffEditorEditors } from '../components/diffEditorEditors.js'; @@ -18,7 +18,7 @@ import { DiffEditorViewModel } from '../diffEditorViewModel.js'; import { PlaceholderViewZone, ViewZoneOverlayWidget, applyStyle, applyViewZones } from '../utils.js'; import { EditorLayoutInfo } from '../../../../common/config/editorOptions.js'; import { LineRange } from '../../../../common/core/ranges/lineRange.js'; -import { OffsetRange, OffsetRangeSet } from '../../../../common/core/offsetRange.js'; +import { OffsetRange, OffsetRangeSet } from '../../../../common/core/ranges/offsetRange.js'; import { MovedText } from '../../../../common/diff/linesDiffComputer.js'; import { localize } from '../../../../../nls.js'; @@ -26,11 +26,11 @@ export class MovedBlocksLinesFeature extends Disposable { public static readonly movedCodeBlockPadding = 4; private readonly _element: SVGElement; - private readonly _originalScrollTop = observableFromEvent(this, this._editors.original.onDidScrollChange, () => this._editors.original.getScrollTop()); - private readonly _modifiedScrollTop = observableFromEvent(this, this._editors.modified.onDidScrollChange, () => this._editors.modified.getScrollTop()); - private readonly _viewZonesChanged = observableSignalFromEvent('onDidChangeViewZones', this._editors.modified.onDidChangeViewZones); + private readonly _originalScrollTop; + private readonly _modifiedScrollTop; + private readonly _viewZonesChanged; - public readonly width = observableValue(this, 0); + public readonly width; constructor( private readonly _rootElement: HTMLElement, @@ -40,6 +40,122 @@ export class MovedBlocksLinesFeature extends Disposable { private readonly _editors: DiffEditorEditors, ) { super(); + this._originalScrollTop = observableFromEvent(this, this._editors.original.onDidScrollChange, () => this._editors.original.getScrollTop()); + this._modifiedScrollTop = observableFromEvent(this, this._editors.modified.onDidScrollChange, () => this._editors.modified.getScrollTop()); + this._viewZonesChanged = observableSignalFromEvent('onDidChangeViewZones', this._editors.modified.onDidChangeViewZones); + this.width = observableValue(this, 0); + this._modifiedViewZonesChangedSignal = observableSignalFromEvent('modified.onDidChangeViewZones', this._editors.modified.onDidChangeViewZones); + this._originalViewZonesChangedSignal = observableSignalFromEvent('original.onDidChangeViewZones', this._editors.original.onDidChangeViewZones); + this._state = derived(this, (reader) => { + /** @description state */ + + this._element.replaceChildren(); + const model = this._diffModel.read(reader); + const moves = model?.diff.read(reader)?.movedTexts; + if (!moves || moves.length === 0) { + this.width.set(0, undefined); + return; + } + + this._viewZonesChanged.read(reader); + + const infoOrig = this._originalEditorLayoutInfo.read(reader); + const infoMod = this._modifiedEditorLayoutInfo.read(reader); + if (!infoOrig || !infoMod) { + this.width.set(0, undefined); + return; + } + + this._modifiedViewZonesChangedSignal.read(reader); + this._originalViewZonesChangedSignal.read(reader); + + const lines = moves.map((move) => { + function computeLineStart(range: LineRange, editor: ICodeEditor) { + const t1 = editor.getTopForLineNumber(range.startLineNumber, true); + const t2 = editor.getTopForLineNumber(range.endLineNumberExclusive, true); + return (t1 + t2) / 2; + } + + const start = computeLineStart(move.lineRangeMapping.original, this._editors.original); + const startOffset = this._originalScrollTop.read(reader); + const end = computeLineStart(move.lineRangeMapping.modified, this._editors.modified); + const endOffset = this._modifiedScrollTop.read(reader); + + const from = start - startOffset; + const to = end - endOffset; + + const top = Math.min(start, end); + const bottom = Math.max(start, end); + + return { range: new OffsetRange(top, bottom), from, to, fromWithoutScroll: start, toWithoutScroll: end, move }; + }); + + lines.sort(tieBreakComparators( + compareBy(l => l.fromWithoutScroll > l.toWithoutScroll, booleanComparator), + compareBy(l => l.fromWithoutScroll > l.toWithoutScroll ? l.fromWithoutScroll : -l.toWithoutScroll, numberComparator) + )); + + const layout = LinesLayout.compute(lines.map(l => l.range)); + + const padding = 10; + const lineAreaLeft = infoOrig.verticalScrollbarWidth; + const lineAreaWidth = (layout.getTrackCount() - 1) * 10 + padding * 2; + const width = lineAreaLeft + lineAreaWidth + (infoMod.contentLeft - MovedBlocksLinesFeature.movedCodeBlockPadding); + + let idx = 0; + for (const line of lines) { + const track = layout.getTrack(idx); + const verticalY = lineAreaLeft + padding + track * 10; + + const arrowHeight = 15; + const arrowWidth = 15; + const right = width; + + const rectWidth = infoMod.glyphMarginWidth + infoMod.lineNumbersWidth; + const rectHeight = 18; + const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); + rect.classList.add('arrow-rectangle'); + rect.setAttribute('x', `${right - rectWidth}`); + rect.setAttribute('y', `${line.to - rectHeight / 2}`); + rect.setAttribute('width', `${rectWidth}`); + rect.setAttribute('height', `${rectHeight}`); + this._element.appendChild(rect); + + const g = document.createElementNS('http://www.w3.org/2000/svg', 'g'); + + const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + + path.setAttribute('d', `M ${0} ${line.from} L ${verticalY} ${line.from} L ${verticalY} ${line.to} L ${right - arrowWidth} ${line.to}`); + path.setAttribute('fill', 'none'); + g.appendChild(path); + + const arrowRight = document.createElementNS('http://www.w3.org/2000/svg', 'polygon'); + arrowRight.classList.add('arrow'); + + reader.store.add(autorun(reader => { + path.classList.toggle('currentMove', line.move === model.activeMovedText.read(reader)); + arrowRight.classList.toggle('currentMove', line.move === model.activeMovedText.read(reader)); + })); + + arrowRight.setAttribute('points', `${right - arrowWidth},${line.to - arrowHeight / 2} ${right},${line.to} ${right - arrowWidth},${line.to + arrowHeight / 2}`); + g.appendChild(arrowRight); + + this._element.appendChild(g); + + /* + TODO@hediet + path.addEventListener('mouseenter', () => { + model.setHoveredMovedText(line.move); + }); + path.addEventListener('mouseleave', () => { + model.setHoveredMovedText(undefined); + });*/ + + idx++; + } + + this.width.set(lineAreaWidth, undefined); + }); this._element = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); this._element.setAttribute('class', 'moved-blocks-lines'); @@ -135,119 +251,10 @@ export class MovedBlocksLinesFeature extends Disposable { })); } - private readonly _modifiedViewZonesChangedSignal = observableSignalFromEvent('modified.onDidChangeViewZones', this._editors.modified.onDidChangeViewZones); - private readonly _originalViewZonesChangedSignal = observableSignalFromEvent('original.onDidChangeViewZones', this._editors.original.onDidChangeViewZones); + private readonly _modifiedViewZonesChangedSignal; + private readonly _originalViewZonesChangedSignal; - private readonly _state = derivedWithStore(this, (reader, store) => { - /** @description state */ - - this._element.replaceChildren(); - const model = this._diffModel.read(reader); - const moves = model?.diff.read(reader)?.movedTexts; - if (!moves || moves.length === 0) { - this.width.set(0, undefined); - return; - } - - this._viewZonesChanged.read(reader); - - const infoOrig = this._originalEditorLayoutInfo.read(reader); - const infoMod = this._modifiedEditorLayoutInfo.read(reader); - if (!infoOrig || !infoMod) { - this.width.set(0, undefined); - return; - } - - this._modifiedViewZonesChangedSignal.read(reader); - this._originalViewZonesChangedSignal.read(reader); - - const lines = moves.map((move) => { - function computeLineStart(range: LineRange, editor: ICodeEditor) { - const t1 = editor.getTopForLineNumber(range.startLineNumber, true); - const t2 = editor.getTopForLineNumber(range.endLineNumberExclusive, true); - return (t1 + t2) / 2; - } - - const start = computeLineStart(move.lineRangeMapping.original, this._editors.original); - const startOffset = this._originalScrollTop.read(reader); - const end = computeLineStart(move.lineRangeMapping.modified, this._editors.modified); - const endOffset = this._modifiedScrollTop.read(reader); - - const from = start - startOffset; - const to = end - endOffset; - - const top = Math.min(start, end); - const bottom = Math.max(start, end); - - return { range: new OffsetRange(top, bottom), from, to, fromWithoutScroll: start, toWithoutScroll: end, move }; - }); - - lines.sort(tieBreakComparators( - compareBy(l => l.fromWithoutScroll > l.toWithoutScroll, booleanComparator), - compareBy(l => l.fromWithoutScroll > l.toWithoutScroll ? l.fromWithoutScroll : -l.toWithoutScroll, numberComparator) - )); - - const layout = LinesLayout.compute(lines.map(l => l.range)); - - const padding = 10; - const lineAreaLeft = infoOrig.verticalScrollbarWidth; - const lineAreaWidth = (layout.getTrackCount() - 1) * 10 + padding * 2; - const width = lineAreaLeft + lineAreaWidth + (infoMod.contentLeft - MovedBlocksLinesFeature.movedCodeBlockPadding); - - let idx = 0; - for (const line of lines) { - const track = layout.getTrack(idx); - const verticalY = lineAreaLeft + padding + track * 10; - - const arrowHeight = 15; - const arrowWidth = 15; - const right = width; - - const rectWidth = infoMod.glyphMarginWidth + infoMod.lineNumbersWidth; - const rectHeight = 18; - const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); - rect.classList.add('arrow-rectangle'); - rect.setAttribute('x', `${right - rectWidth}`); - rect.setAttribute('y', `${line.to - rectHeight / 2}`); - rect.setAttribute('width', `${rectWidth}`); - rect.setAttribute('height', `${rectHeight}`); - this._element.appendChild(rect); - - const g = document.createElementNS('http://www.w3.org/2000/svg', 'g'); - - const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); - - path.setAttribute('d', `M ${0} ${line.from} L ${verticalY} ${line.from} L ${verticalY} ${line.to} L ${right - arrowWidth} ${line.to}`); - path.setAttribute('fill', 'none'); - g.appendChild(path); - - const arrowRight = document.createElementNS('http://www.w3.org/2000/svg', 'polygon'); - arrowRight.classList.add('arrow'); - - store.add(autorun(reader => { - path.classList.toggle('currentMove', line.move === model.activeMovedText.read(reader)); - arrowRight.classList.toggle('currentMove', line.move === model.activeMovedText.read(reader)); - })); - - arrowRight.setAttribute('points', `${right - arrowWidth},${line.to - arrowHeight / 2} ${right},${line.to} ${right - arrowWidth},${line.to + arrowHeight / 2}`); - g.appendChild(arrowRight); - - this._element.appendChild(g); - - /* - TODO@hediet - path.addEventListener('mouseenter', () => { - model.setHoveredMovedText(line.move); - }); - path.addEventListener('mouseleave', () => { - model.setHoveredMovedText(undefined); - });*/ - - idx++; - } - - this.width.set(lineAreaWidth, undefined); - }); + private readonly _state; } class LinesLayout { diff --git a/src/vs/editor/browser/widget/diffEditor/features/revertButtonsFeature.ts b/src/vs/editor/browser/widget/diffEditor/features/revertButtonsFeature.ts index 76c1c664896..57768764dad 100644 --- a/src/vs/editor/browser/widget/diffEditor/features/revertButtonsFeature.ts +++ b/src/vs/editor/browser/widget/diffEditor/features/revertButtonsFeature.ts @@ -107,17 +107,11 @@ export class RevertButtonsFeature extends Disposable { export class RevertButton extends Disposable implements IGlyphMarginWidget { public static counter = 0; - private readonly _id: string = `revertButton${RevertButton.counter++}`; + private readonly _id: string; getId(): string { return this._id; } - private readonly _domNode = h('div.revertButton', { - title: this._revertSelection - ? localize('revertSelectedChanges', 'Revert Selected Changes') - : localize('revertChange', 'Revert Change') - }, - [renderIcon(Codicon.arrowRight)] - ).root; + private readonly _domNode; constructor( private readonly _lineNumber: number, @@ -126,6 +120,14 @@ export class RevertButton extends Disposable implements IGlyphMarginWidget { private readonly _revertSelection: boolean, ) { super(); + this._id = `revertButton${RevertButton.counter++}`; + this._domNode = h('div.revertButton', { + title: this._revertSelection + ? localize('revertSelectedChanges', 'Revert Selected Changes') + : localize('revertChange', 'Revert Change') + }, + [renderIcon(Codicon.arrowRight)] + ).root; this._register(addDisposableListener(this._domNode, EventType.MOUSE_DOWN, e => { diff --git a/src/vs/editor/browser/widget/diffEditor/utils.ts b/src/vs/editor/browser/widget/diffEditor/utils.ts index 8fbcaddc48a..c1579dacef5 100644 --- a/src/vs/editor/browser/widget/diffEditor/utils.ts +++ b/src/vs/editor/browser/widget/diffEditor/utils.ts @@ -14,7 +14,7 @@ import { Position } from '../../../common/core/position.js'; import { Range } from '../../../common/core/range.js'; import { DetailedLineRangeMapping } from '../../../common/diff/rangeMapping.js'; import { IModelDeltaDecoration } from '../../../common/model.js'; -import { TextLength } from '../../../common/core/textLength.js'; +import { TextLength } from '../../../common/core/text/textLength.js'; export function joinCombine(arr1: readonly T[], arr2: readonly T[], keySelector: (val: T) => number, combine: (v1: T, v2: T) => T): readonly T[] { if (arr1.length === 0) { @@ -221,33 +221,42 @@ export interface IObservableViewZone extends IViewZone { } export class PlaceholderViewZone implements IObservableViewZone { - public readonly domNode = document.createElement('div'); + public readonly domNode; - private readonly _actualTop = observableValue(this, undefined); - private readonly _actualHeight = observableValue(this, undefined); + private readonly _actualTop; + private readonly _actualHeight; - public readonly actualTop: IObservable = this._actualTop; - public readonly actualHeight: IObservable = this._actualHeight; + public readonly actualTop: IObservable; + public readonly actualHeight: IObservable; - public readonly showInHiddenAreas = true; + public readonly showInHiddenAreas; public get afterLineNumber(): number { return this._afterLineNumber.get(); } - public readonly onChange?: IObservable = this._afterLineNumber; + public readonly onChange?: IObservable; constructor( private readonly _afterLineNumber: IObservable, public readonly heightInPx: number, ) { + this.domNode = document.createElement('div'); + this._actualTop = observableValue(this, undefined); + this._actualHeight = observableValue(this, undefined); + this.actualTop = this._actualTop; + this.actualHeight = this._actualHeight; + this.showInHiddenAreas = true; + this.onChange = this._afterLineNumber; + this.onDomNodeTop = (top: number) => { + this._actualTop.set(top, undefined); + }; + this.onComputedHeight = (height: number) => { + this._actualHeight.set(height, undefined); + }; } - onDomNodeTop = (top: number) => { - this._actualTop.set(top, undefined); - }; + onDomNodeTop; - onComputedHeight = (height: number) => { - this._actualHeight.set(height, undefined); - }; + onComputedHeight; } diff --git a/src/vs/editor/browser/widget/diffEditor/utils/editorGutter.ts b/src/vs/editor/browser/widget/diffEditor/utils/editorGutter.ts index ee601aa83e1..86e17fdde6c 100644 --- a/src/vs/editor/browser/widget/diffEditor/utils/editorGutter.ts +++ b/src/vs/editor/browser/widget/diffEditor/utils/editorGutter.ts @@ -8,22 +8,16 @@ import { Disposable, IDisposable, toDisposable } from '../../../../../base/commo import { autorun, IObservable, IReader, ISettableObservable, observableFromEvent, observableSignal, observableSignalFromEvent, observableValue, transaction } from '../../../../../base/common/observable.js'; import { CodeEditorWidget } from '../../codeEditor/codeEditorWidget.js'; import { LineRange } from '../../../../common/core/ranges/lineRange.js'; -import { OffsetRange } from '../../../../common/core/offsetRange.js'; +import { OffsetRange } from '../../../../common/core/ranges/offsetRange.js'; export class EditorGutter extends Disposable { - private readonly scrollTop = observableFromEvent(this, - this._editor.onDidScrollChange, - (e) => /** @description editor.onDidScrollChange */ this._editor.getScrollTop() - ); - private readonly isScrollTopZero = this.scrollTop.map((scrollTop) => /** @description isScrollTopZero */ scrollTop === 0); - private readonly modelAttached = observableFromEvent(this, - this._editor.onDidChangeModel, - (e) => /** @description editor.onDidChangeModel */ this._editor.hasModel() - ); + private readonly scrollTop; + private readonly isScrollTopZero; + private readonly modelAttached; - private readonly editorOnDidChangeViewZones = observableSignalFromEvent('onDidChangeViewZones', this._editor.onDidChangeViewZones); - private readonly editorOnDidContentSizeChange = observableSignalFromEvent('onDidContentSizeChange', this._editor.onDidContentSizeChange); - private readonly domNodeSizeChanged = observableSignal('domNodeSizeChanged'); + private readonly editorOnDidChangeViewZones; + private readonly editorOnDidContentSizeChange; + private readonly domNodeSizeChanged; constructor( private readonly _editor: CodeEditorWidget, @@ -31,6 +25,19 @@ export class EditorGutter extends D private readonly itemProvider: IGutterItemProvider ) { super(); + this.scrollTop = observableFromEvent(this, + this._editor.onDidScrollChange, + (e) => /** @description editor.onDidScrollChange */ this._editor.getScrollTop() + ); + this.isScrollTopZero = this.scrollTop.map((scrollTop) => /** @description isScrollTopZero */ scrollTop === 0); + this.modelAttached = observableFromEvent(this, + this._editor.onDidChangeModel, + (e) => /** @description editor.onDidChangeModel */ this._editor.hasModel() + ); + this.editorOnDidChangeViewZones = observableSignalFromEvent('onDidChangeViewZones', this._editor.onDidChangeViewZones); + this.editorOnDidContentSizeChange = observableSignalFromEvent('onDidContentSizeChange', this._editor.onDidContentSizeChange); + this.domNodeSizeChanged = observableSignal('domNodeSizeChanged'); + this.views = new Map(); this._domNode.className = 'gutter monaco-editor'; const scrollDecoration = this._domNode.appendChild( h('div.scroll-decoration', { role: 'presentation', ariaHidden: 'true', style: { width: '100%' } }) @@ -60,7 +67,7 @@ export class EditorGutter extends D reset(this._domNode); } - private readonly views = new Map(); + private readonly views; private render(reader: IReader): void { if (!this.modelAttached.read(reader)) { diff --git a/src/vs/editor/browser/widget/multiDiffEditor/diffEditorItemTemplate.ts b/src/vs/editor/browser/widget/multiDiffEditor/diffEditorItemTemplate.ts index 48ac35815d4..70a99958b3c 100644 --- a/src/vs/editor/browser/widget/multiDiffEditor/diffEditorItemTemplate.ts +++ b/src/vs/editor/browser/widget/multiDiffEditor/diffEditorItemTemplate.ts @@ -14,7 +14,7 @@ import { IContextKeyService, type IScopedContextKeyService } from '../../../../p import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js'; import { IDiffEditorOptions } from '../../../common/config/editorOptions.js'; -import { OffsetRange } from '../../../common/core/offsetRange.js'; +import { OffsetRange } from '../../../common/core/ranges/offsetRange.js'; import { observableCodeEditor } from '../../observableCodeEditor.js'; import { DiffEditorWidget } from '../diffEditor/diffEditorWidget.js'; import { DocumentDiffItemViewModel } from './multiDiffEditorViewModel.js'; @@ -35,64 +35,31 @@ export class TemplateData implements IObjectData { } export class DiffEditorItemTemplate extends Disposable implements IPooledObject { - private readonly _viewModel = observableValue(this, undefined); + private readonly _viewModel; - private readonly _collapsed = derived(this, reader => this._viewModel.read(reader)?.collapsed.read(reader)); + private readonly _collapsed; - private readonly _editorContentHeight = observableValue(this, 500); - public readonly contentHeight = derived(this, reader => { - const h = this._collapsed.read(reader) ? 0 : this._editorContentHeight.read(reader); - return h + this._outerEditorHeight; - }); + private readonly _editorContentHeight; + public readonly contentHeight; - private readonly _modifiedContentWidth = observableValue(this, 0); - private readonly _modifiedWidth = observableValue(this, 0); - private readonly _originalContentWidth = observableValue(this, 0); - private readonly _originalWidth = observableValue(this, 0); + private readonly _modifiedContentWidth; + private readonly _modifiedWidth; + private readonly _originalContentWidth; + private readonly _originalWidth; - public readonly maxScroll = derived(this, reader => { - const scroll1 = this._modifiedContentWidth.read(reader) - this._modifiedWidth.read(reader); - const scroll2 = this._originalContentWidth.read(reader) - this._originalWidth.read(reader); - if (scroll1 > scroll2) { - return { maxScroll: scroll1, width: this._modifiedWidth.read(reader) }; - } else { - return { maxScroll: scroll2, width: this._originalWidth.read(reader) }; - } - }); + public readonly maxScroll; - private readonly _elements = h('div.multiDiffEntry', [ - h('div.header@header', [ - h('div.header-content', [ - h('div.collapse-button@collapseButton'), - h('div.file-path', [ - h('div.title.modified.show-file-icons@primaryPath', [] as any), - h('div.status.deleted@status', ['R']), - h('div.title.original.show-file-icons@secondaryPath', [] as any), - ]), - h('div.actions@actions'), - ]), - ]), + private readonly _elements; - h('div.editorParent', [ - h('div.editorContainer@editor'), - ]) - ]) as Record; + public readonly editor; - public readonly editor = this._register(this._instantiationService.createInstance(DiffEditorWidget, this._elements.editor, { - overflowWidgetsDomNode: this._overflowWidgetsDomNode, - }, {})); + private readonly isModifedFocused; + private readonly isOriginalFocused; + public readonly isFocused; - private readonly isModifedFocused = observableCodeEditor(this.editor.getModifiedEditor()).isFocused; - private readonly isOriginalFocused = observableCodeEditor(this.editor.getOriginalEditor()).isFocused; - public readonly isFocused = derived(this, reader => this.isModifedFocused.read(reader) || this.isOriginalFocused.read(reader)); + private readonly _resourceLabel; - private readonly _resourceLabel = this._workbenchUIElementFactory.createResourceLabel - ? this._register(this._workbenchUIElementFactory.createResourceLabel(this._elements.primaryPath)) - : undefined; - - private readonly _resourceLabel2 = this._workbenchUIElementFactory.createResourceLabel - ? this._register(this._workbenchUIElementFactory.createResourceLabel(this._elements.secondaryPath)) - : undefined; + private readonly _resourceLabel2; private readonly _outerEditorHeight: number; private readonly _contextKeyService: IScopedContextKeyService; @@ -105,6 +72,59 @@ export class DiffEditorItemTemplate extends Disposable implements IPooledObject< @IContextKeyService _parentContextKeyService: IContextKeyService, ) { super(); + this._viewModel = observableValue(this, undefined); + this._collapsed = derived(this, reader => this._viewModel.read(reader)?.collapsed.read(reader)); + this._editorContentHeight = observableValue(this, 500); + this.contentHeight = derived(this, reader => { + const h = this._collapsed.read(reader) ? 0 : this._editorContentHeight.read(reader); + return h + this._outerEditorHeight; + }); + this._modifiedContentWidth = observableValue(this, 0); + this._modifiedWidth = observableValue(this, 0); + this._originalContentWidth = observableValue(this, 0); + this._originalWidth = observableValue(this, 0); + this.maxScroll = derived(this, reader => { + const scroll1 = this._modifiedContentWidth.read(reader) - this._modifiedWidth.read(reader); + const scroll2 = this._originalContentWidth.read(reader) - this._originalWidth.read(reader); + if (scroll1 > scroll2) { + return { maxScroll: scroll1, width: this._modifiedWidth.read(reader) }; + } else { + return { maxScroll: scroll2, width: this._originalWidth.read(reader) }; + } + }); + this._elements = h('div.multiDiffEntry', [ + h('div.header@header', [ + h('div.header-content', [ + h('div.collapse-button@collapseButton'), + h('div.file-path', [ + h('div.title.modified.show-file-icons@primaryPath', [] as any), + h('div.status.deleted@status', ['R']), + h('div.title.original.show-file-icons@secondaryPath', [] as any), + ]), + h('div.actions@actions'), + ]), + ]), + + h('div.editorParent', [ + h('div.editorContainer@editor'), + ]) + ]) as Record; + this.editor = this._register(this._instantiationService.createInstance(DiffEditorWidget, this._elements.editor, { + overflowWidgetsDomNode: this._overflowWidgetsDomNode, + }, {})); + this.isModifedFocused = observableCodeEditor(this.editor.getModifiedEditor()).isFocused; + this.isOriginalFocused = observableCodeEditor(this.editor.getOriginalEditor()).isFocused; + this.isFocused = derived(this, reader => this.isModifedFocused.read(reader) || this.isOriginalFocused.read(reader)); + this._resourceLabel = this._workbenchUIElementFactory.createResourceLabel + ? this._register(this._workbenchUIElementFactory.createResourceLabel(this._elements.primaryPath)) + : undefined; + this._resourceLabel2 = this._workbenchUIElementFactory.createResourceLabel + ? this._register(this._workbenchUIElementFactory.createResourceLabel(this._elements.secondaryPath)) + : undefined; + this._dataStore = this._register(new DisposableStore()); + this._headerHeight = 40; + this._lastScrollTop = -1; + this._isSettingScrollTop = false; const btn = new Button(this._elements.collapseButton, {}); @@ -178,7 +198,7 @@ export class DiffEditorItemTemplate extends Disposable implements IPooledObject< } } - private readonly _dataStore = this._register(new DisposableStore()); + private readonly _dataStore; private _data: TemplateData | undefined; @@ -261,10 +281,10 @@ export class DiffEditorItemTemplate extends Disposable implements IPooledObject< } } - private readonly _headerHeight = /*this._elements.header.clientHeight*/ 40; + private readonly _headerHeight; - private _lastScrollTop = -1; - private _isSettingScrollTop = false; + private _lastScrollTop; + private _isSettingScrollTop; public render(verticalRange: OffsetRange, width: number, editorScroll: number, viewPort: OffsetRange): void { this._elements.root.style.visibility = 'visible'; diff --git a/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel.ts b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel.ts index 49c674d0477..fd4c9bae7ba 100644 --- a/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel.ts +++ b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel.ts @@ -18,26 +18,16 @@ import { RefCounted } from '../diffEditor/utils.js'; import { IDocumentDiffItem, IMultiDiffEditorModel } from './model.js'; export class MultiDiffEditorViewModel extends Disposable { - private readonly _documents: IObservable[] | 'loading'> = observableFromValueWithChangeEvent(this.model, this.model.documents); + private readonly _documents: IObservable[] | 'loading'>; - private readonly _documentsArr = derived(this, reader => { - const result = this._documents.read(reader); - if (result === 'loading') { return []; } - return result; - }); + private readonly _documentsArr; - public readonly isLoading = derived(this, reader => this._documents.read(reader) === 'loading'); + public readonly isLoading; - public readonly items: IObservable = mapObservableArrayCached( - this, - this._documentsArr, - (d, store) => store.add(this._instantiationService.createInstance(DocumentDiffItemViewModel, d, this)) - ).recomputeInitiallyAndOnChange(this._store); + public readonly items: IObservable; - public readonly focusedDiffItem = derived(this, reader => this.items.read(reader).find(i => i.isFocused.read(reader))); - public readonly activeDiffItem = derivedObservableWithWritableCache(this, - (reader, lastValue) => this.focusedDiffItem.read(reader) ?? (lastValue && this.items.read(reader).indexOf(lastValue) !== -1) ? lastValue : undefined - ); + public readonly focusedDiffItem; + public readonly activeDiffItem; public async waitForDiffs(): Promise { for (const d of this.items.get()) { @@ -70,6 +60,22 @@ export class MultiDiffEditorViewModel extends Disposable { private readonly _instantiationService: IInstantiationService, ) { super(); + this._documents = observableFromValueWithChangeEvent(this.model, this.model.documents); + this._documentsArr = derived(this, reader => { + const result = this._documents.read(reader); + if (result === 'loading') { return []; } + return result; + }); + this.isLoading = derived(this, reader => this._documents.read(reader) === 'loading'); + this.items = mapObservableArrayCached( + this, + this._documentsArr, + (d, store) => store.add(this._instantiationService.createInstance(DocumentDiffItemViewModel, d, this)) + ).recomputeInitiallyAndOnChange(this._store); + this.focusedDiffItem = derived(this, reader => this.items.read(reader).find(i => i.isFocused.read(reader))); + this.activeDiffItem = derivedObservableWithWritableCache(this, + (reader, lastValue) => this.focusedDiffItem.read(reader) ?? (lastValue && this.items.read(reader).indexOf(lastValue) !== -1) ? lastValue : undefined + ); } } diff --git a/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidget.ts b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidget.ts index d23ad8c87f6..9c49808167e 100644 --- a/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidget.ts +++ b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidget.ts @@ -7,7 +7,7 @@ import { Dimension } from '../../../../base/browser/dom.js'; import { Event } from '../../../../base/common/event.js'; import { readHotReloadableExport } from '../../../../base/common/hotReloadHelpers.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; -import { derived, derivedWithStore, observableValue, recomputeInitiallyAndOnChange } from '../../../../base/common/observable.js'; +import { derived, observableValue, recomputeInitiallyAndOnChange } from '../../../../base/common/observable.js'; import { URI } from '../../../../base/common/uri.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { Range } from '../../../common/core/range.js'; @@ -25,9 +25,9 @@ export class MultiDiffEditorWidget extends Disposable { private readonly _dimension = observableValue(this, undefined); private readonly _viewModel = observableValue(this, undefined); - private readonly _widgetImpl = derivedWithStore(this, (reader, store) => { + private readonly _widgetImpl = derived(this, (reader) => { readHotReloadableExport(DiffEditorItemTemplate, reader); - return store.add(this._instantiationService.createInstance(( + return reader.store.add(this._instantiationService.createInstance(( readHotReloadableExport(MultiDiffEditorWidgetImpl, reader)), this._element, this._dimension, diff --git a/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.ts b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.ts index ec0bf3cf64c..288d0bd8678 100644 --- a/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.ts +++ b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.ts @@ -9,7 +9,7 @@ import { compareBy, numberComparator } from '../../../../base/common/arrays.js'; import { findFirstMax } from '../../../../base/common/arraysFind.js'; import { BugIndicatingError } from '../../../../base/common/errors.js'; import { Disposable, IReference, toDisposable } from '../../../../base/common/lifecycle.js'; -import { IObservable, IReader, ITransaction, autorun, autorunWithStore, derived, derivedWithStore, disposableObservableValue, globalTransaction, observableFromEvent, observableValue, transaction } from '../../../../base/common/observable.js'; +import { IObservable, IReader, ITransaction, autorun, autorunWithStore, derived, disposableObservableValue, globalTransaction, observableFromEvent, observableValue, transaction } from '../../../../base/common/observable.js'; import { Scrollable, ScrollbarVisibility } from '../../../../base/common/scrollable.js'; import { URI } from '../../../../base/common/uri.js'; import { localize } from '../../../../nls.js'; @@ -17,7 +17,7 @@ import { ContextKeyValue, IContextKeyService } from '../../../../platform/contex import { ITextEditorOptions } from '../../../../platform/editor/common/editor.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js'; -import { OffsetRange } from '../../../common/core/offsetRange.js'; +import { OffsetRange } from '../../../common/core/ranges/offsetRange.js'; import { IRange } from '../../../common/core/range.js'; import { ISelection, Selection } from '../../../common/core/selection.js'; import { IDiffEditor } from '../../../common/editorCommon.js'; @@ -33,90 +33,32 @@ import './style.css'; import { IWorkbenchUIElementFactory } from './workbenchUIElementFactory.js'; export class MultiDiffEditorWidgetImpl extends Disposable { - private readonly _scrollableElements = h('div.scrollContent', [ - h('div@content', { - style: { - overflow: 'hidden', - } - }), - h('div.monaco-editor@overflowWidgetsDomNode', { - }), - ]); + private readonly _scrollableElements; - private readonly _scrollable = this._register(new Scrollable({ - forceIntegerValues: false, - scheduleAtNextAnimationFrame: (cb) => scheduleAtNextAnimationFrame(getWindow(this._element), cb), - smoothScrollDuration: 100, - })); + private readonly _scrollable; - private readonly _scrollableElement = this._register(new SmoothScrollableElement(this._scrollableElements.root, { - vertical: ScrollbarVisibility.Auto, - horizontal: ScrollbarVisibility.Auto, - useShadows: false, - }, this._scrollable)); + private readonly _scrollableElement; - private readonly _elements = h('div.monaco-component.multiDiffEditor', {}, [ - h('div', {}, [this._scrollableElement.getDomNode()]), - h('div.placeholder@placeholder', {}, [h('div')]), - ]); + private readonly _elements; - private readonly _sizeObserver = this._register(new ObservableElementSizeObserver(this._element, undefined)); + private readonly _sizeObserver; - private readonly _objectPool = this._register(new ObjectPool((data) => { - const template = this._instantiationService.createInstance( - DiffEditorItemTemplate, - this._scrollableElements.content, - this._scrollableElements.overflowWidgetsDomNode, - this._workbenchUIElementFactory - ); - template.setData(data); - return template; - })); + private readonly _objectPool; - public readonly scrollTop = observableFromEvent(this, this._scrollableElement.onScroll, () => /** @description scrollTop */ this._scrollableElement.getScrollPosition().scrollTop); - public readonly scrollLeft = observableFromEvent(this, this._scrollableElement.onScroll, () => /** @description scrollLeft */ this._scrollableElement.getScrollPosition().scrollLeft); + public readonly scrollTop; + public readonly scrollLeft; - private readonly _viewItemsInfo = derivedWithStore<{ items: readonly VirtualizedViewItem[]; getItem: (viewModel: DocumentDiffItemViewModel) => VirtualizedViewItem }>(this, - (reader, store) => { - const vm = this._viewModel.read(reader); - if (!vm) { - return { items: [], getItem: _d => { throw new BugIndicatingError(); } }; - } - const viewModels = vm.items.read(reader); - const map = new Map(); - const items = viewModels.map(d => { - const item = store.add(new VirtualizedViewItem(d, this._objectPool, this.scrollLeft, delta => { - this._scrollableElement.setScrollPosition({ scrollTop: this._scrollableElement.getScrollPosition().scrollTop + delta }); - })); - const data = this._lastDocStates?.[item.getKey()]; - if (data) { - transaction(tx => { - item.setViewState(data, tx); - }); - } - map.set(d, item); - return item; - }); - return { items, getItem: d => map.get(d)! }; - } - ); + private readonly _viewItemsInfo; - private readonly _viewItems = this._viewItemsInfo.map(this, items => items.items); + private readonly _viewItems; - private readonly _spaceBetweenPx = 0; + private readonly _spaceBetweenPx; - private readonly _totalHeight = this._viewItems.map(this, (items, reader) => items.reduce((r, i) => r + i.contentHeight.read(reader) + this._spaceBetweenPx, 0)); - public readonly activeControl = derived(this, reader => { - const activeDiffItem = this._viewModel.read(reader)?.activeDiffItem.read(reader); - if (!activeDiffItem) { return undefined; } - const viewItem = this._viewItemsInfo.read(reader).getItem(activeDiffItem); - return viewItem.template.read(reader)?.editor; - }); + private readonly _totalHeight; + public readonly activeControl; - private readonly _contextKeyService = this._register(this._parentContextKeyService.createScoped(this._element)); - private readonly _instantiationService = this._register(this._parentInstantiationService.createChild( - new ServiceCollection([IContextKeyService, this._contextKeyService]) - )); + private readonly _contextKeyService; + private readonly _instantiationService; constructor( private readonly _element: HTMLElement, @@ -127,6 +69,80 @@ export class MultiDiffEditorWidgetImpl extends Disposable { @IInstantiationService private readonly _parentInstantiationService: IInstantiationService, ) { super(); + this._scrollableElements = h('div.scrollContent', [ + h('div@content', { + style: { + overflow: 'hidden', + } + }), + h('div.monaco-editor@overflowWidgetsDomNode', { + }), + ]); + this._scrollable = this._register(new Scrollable({ + forceIntegerValues: false, + scheduleAtNextAnimationFrame: (cb) => scheduleAtNextAnimationFrame(getWindow(this._element), cb), + smoothScrollDuration: 100, + })); + this._scrollableElement = this._register(new SmoothScrollableElement(this._scrollableElements.root, { + vertical: ScrollbarVisibility.Auto, + horizontal: ScrollbarVisibility.Auto, + useShadows: false, + }, this._scrollable)); + this._elements = h('div.monaco-component.multiDiffEditor', {}, [ + h('div', {}, [this._scrollableElement.getDomNode()]), + h('div.placeholder@placeholder', {}, [h('div')]), + ]); + this._sizeObserver = this._register(new ObservableElementSizeObserver(this._element, undefined)); + this._objectPool = this._register(new ObjectPool((data) => { + const template = this._instantiationService.createInstance( + DiffEditorItemTemplate, + this._scrollableElements.content, + this._scrollableElements.overflowWidgetsDomNode, + this._workbenchUIElementFactory + ); + template.setData(data); + return template; + })); + this.scrollTop = observableFromEvent(this, this._scrollableElement.onScroll, () => /** @description scrollTop */ this._scrollableElement.getScrollPosition().scrollTop); + this.scrollLeft = observableFromEvent(this, this._scrollableElement.onScroll, () => /** @description scrollLeft */ this._scrollableElement.getScrollPosition().scrollLeft); + this._viewItemsInfo = derived<{ items: readonly VirtualizedViewItem[]; getItem: (viewModel: DocumentDiffItemViewModel) => VirtualizedViewItem }>(this, + (reader) => { + const vm = this._viewModel.read(reader); + if (!vm) { + return { items: [], getItem: _d => { throw new BugIndicatingError(); } }; + } + const viewModels = vm.items.read(reader); + const map = new Map(); + const items = viewModels.map(d => { + const item = reader.store.add(new VirtualizedViewItem(d, this._objectPool, this.scrollLeft, delta => { + this._scrollableElement.setScrollPosition({ scrollTop: this._scrollableElement.getScrollPosition().scrollTop + delta }); + })); + const data = this._lastDocStates?.[item.getKey()]; + if (data) { + transaction(tx => { + item.setViewState(data, tx); + }); + } + map.set(d, item); + return item; + }); + return { items, getItem: d => map.get(d)! }; + } + ); + this._viewItems = this._viewItemsInfo.map(this, items => items.items); + this._spaceBetweenPx = 0; + this._totalHeight = this._viewItems.map(this, (items, reader) => items.reduce((r, i) => r + i.contentHeight.read(reader) + this._spaceBetweenPx, 0)); + this.activeControl = derived(this, reader => { + const activeDiffItem = this._viewModel.read(reader)?.activeDiffItem.read(reader); + if (!activeDiffItem) { return undefined; } + const viewItem = this._viewItemsInfo.read(reader).getItem(activeDiffItem); + return viewItem.template.read(reader)?.editor; + }); + this._contextKeyService = this._register(this._parentContextKeyService.createScoped(this._element)); + this._instantiationService = this._register(this._parentInstantiationService.createChild( + new ServiceCollection([IContextKeyService, this._contextKeyService]) + )); + this._lastDocStates = {}; this._register(autorunWithStore((reader, store) => { const viewModel = this._viewModel.read(reader); @@ -251,7 +267,7 @@ export class MultiDiffEditorWidgetImpl extends Disposable { } /** This accounts for documents that are not loaded yet. */ - private _lastDocStates: IMultiDiffEditorViewState['docStates'] = {}; + private _lastDocStates: IMultiDiffEditorViewState['docStates']; public setViewState(viewState: IMultiDiffEditorViewState): void { this.setScrollState(viewState.scrollState); diff --git a/src/vs/editor/common/codecs/utils/objectStream.ts b/src/vs/editor/common/codecs/utils/objectStream.ts index 21183819921..394574f74b0 100644 --- a/src/vs/editor/common/codecs/utils/objectStream.ts +++ b/src/vs/editor/common/codecs/utils/objectStream.ts @@ -5,7 +5,7 @@ import { ITextModel } from '../../model.js'; import { VSBuffer } from '../../../../base/common/buffer.js'; -import { assert, assertNever } from '../../../../base/common/assert.js'; +import { assertNever } from '../../../../base/common/assert.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { ObservableDisposable } from '../../../../base/common/observableDisposable.js'; import { newWriteableStream, WriteableStream, ReadableStream } from '../../../../base/common/stream.js'; @@ -56,26 +56,17 @@ export class ObjectStream extends ObservableDisposable impleme public send( stopAfterFirstSend: boolean = false, ): void { - if (this.cancellationToken?.isCancellationRequested) { + // this method can be called asynchronously by the `setTimeout` utility below, hence + // the state of the cancellation token or the stream itself might have changed by that time + if (this.cancellationToken?.isCancellationRequested || this.ended) { this.end(); return; } - assert( - this.ended === false, - 'Cannot send on already ended stream.', - ); - this.sendData() .then(() => { - if (this.cancellationToken?.isCancellationRequested) { - this.end(); - - return; - } - - if (this.ended) { + if (this.cancellationToken?.isCancellationRequested || this.ended) { this.end(); return; diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 9e200454323..c4343088541 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -16,7 +16,6 @@ import { USUAL_WORD_SEPARATORS } from '../core/wordHelper.js'; import * as nls from '../../../nls.js'; import { AccessibilitySupport } from '../../../platform/accessibility/common/accessibility.js'; import { IConfigurationPropertySchema } from '../../../platform/configuration/common/configurationRegistry.js'; -import product from '../../../platform/product/common/product.js'; //#region typed options @@ -54,6 +53,10 @@ export interface IEditorOptions { * This editor is used inside a diff editor. */ inDiffEditor?: boolean; + /** + * This editor is allowed to use variable line heights. + */ + allowVariableLineHeights?: boolean; /** * The aria label for the editor's textarea (when it is focused). */ @@ -5450,6 +5453,7 @@ export const enum EditorOption { acceptSuggestionOnEnter, accessibilitySupport, accessibilityPageSize, + allowVariableLineHeights, ariaLabel, ariaRequired, autoClosingBrackets, @@ -5630,6 +5634,9 @@ export const EditorOptions = { description: nls.localize('accessibilityPageSize', "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'] })), + allowVariableLineHeights: register(new EditorBooleanOption( + EditorOption.allowVariableLineHeights, 'allowVariableLineHeights', true + )), ariaLabel: register(new EditorStringOption( EditorOption.ariaLabel, 'ariaLabel', nls.localize('editorViewAccessibleLabel', "Editor content") )), @@ -5867,7 +5874,7 @@ export const EditorOptions = { emptySelectionClipboard: register(new EditorEmptySelectionClipboard()), dropIntoEditor: register(new EditorDropIntoEditor()), experimentalEditContextEnabled: register(new EditorBooleanOption( - EditorOption.experimentalEditContextEnabled, 'experimentalEditContextEnabled', product.quality !== 'stable', + EditorOption.experimentalEditContextEnabled, 'experimentalEditContextEnabled', true, { description: nls.localize('experimentalEditContextEnabled', "Sets whether the new experimental edit context should be used instead of the text area."), included: platform.isChrome || platform.isEdge || platform.isNative diff --git a/src/vs/editor/common/core/2d/rect.ts b/src/vs/editor/common/core/2d/rect.ts index edafc15854a..8804d46ec6b 100644 --- a/src/vs/editor/common/core/2d/rect.ts +++ b/src/vs/editor/common/core/2d/rect.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { BugIndicatingError } from '../../../../base/common/errors.js'; -import { OffsetRange } from '../offsetRange.js'; +import { OffsetRange } from '../ranges/offsetRange.js'; import { Point } from './point.js'; export class Rect { diff --git a/src/vs/editor/common/core/edits/arrayEdit.ts b/src/vs/editor/common/core/edits/arrayEdit.ts index f5905c2c164..5fbcbc597f7 100644 --- a/src/vs/editor/common/core/edits/arrayEdit.ts +++ b/src/vs/editor/common/core/edits/arrayEdit.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { OffsetRange } from '../offsetRange.js'; +import { OffsetRange } from '../ranges/offsetRange.js'; import { BaseEdit, BaseReplacement } from './edit.js'; /** diff --git a/src/vs/editor/common/core/edits/edit.ts b/src/vs/editor/common/core/edits/edit.ts index 9271cb6f1a1..c364e96bec9 100644 --- a/src/vs/editor/common/core/edits/edit.ts +++ b/src/vs/editor/common/core/edits/edit.ts @@ -5,10 +5,10 @@ import { sumBy } from '../../../../base/common/arrays.js'; import { BugIndicatingError } from '../../../../base/common/errors.js'; -import { OffsetRange } from '../offsetRange.js'; +import { OffsetRange } from '../ranges/offsetRange.js'; export abstract class BaseEdit, TEdit extends BaseEdit> { - protected constructor( + constructor( public readonly replacements: readonly T[], ) { let lastEndEx = -1; @@ -22,6 +22,11 @@ export abstract class BaseEdit, TEdit extends BaseE protected abstract _createNew(replacements: readonly T[]): TEdit; + /** + * Returns true if and only if this edit and the given edit are structurally equal. + * Note that this does not mean that the edits have the same effect on a given input! + * See `.normalize()` or `.normalizeOnBase(base)` for that. + */ public equals(other: TEdit): boolean { if (this.replacements.length !== other.replacements.length) { return false; @@ -178,6 +183,9 @@ export abstract class BaseEdit, TEdit extends BaseE return this._createNew(result).normalize(); } + /** + * Returns the range of each replacement in the applied value. + */ public getNewRanges(): OffsetRange[] { const ranges: OffsetRange[] = []; let offset = 0; @@ -202,6 +210,50 @@ export abstract class BaseEdit, TEdit extends BaseE public getLengthDelta(): number { return sumBy(this.replacements, (replacement) => replacement.getLengthDelta()); } + + public getNewDataLength(dataLength: number): number { + return dataLength + this.getLengthDelta(); + } + + public applyToOffset(originalOffset: number): number { + let accumulatedDelta = 0; + for (const r of this.replacements) { + if (r.replaceRange.start <= originalOffset) { + if (originalOffset < r.replaceRange.endExclusive) { + // the offset is in the replaced range + return r.replaceRange.start + accumulatedDelta; + } + accumulatedDelta += r.getNewLength() - r.replaceRange.length; + } else { + break; + } + } + return originalOffset + accumulatedDelta; + } + + public applyToOffsetRange(originalRange: OffsetRange): OffsetRange { + return new OffsetRange( + this.applyToOffset(originalRange.start), + this.applyToOffset(originalRange.endExclusive) + ); + } + + public applyInverseToOffset(postEditsOffset: number): number { + let accumulatedDelta = 0; + for (const edit of this.replacements) { + const editLength = edit.getNewLength(); + if (edit.replaceRange.start <= postEditsOffset - accumulatedDelta) { + if (postEditsOffset - accumulatedDelta < edit.replaceRange.start + editLength) { + // the offset is in the replaced range + return edit.replaceRange.start; + } + accumulatedDelta += editLength - edit.replaceRange.length; + } else { + break; + } + } + return postEditsOffset - accumulatedDelta; + } } export abstract class BaseReplacement> { @@ -234,8 +286,19 @@ export abstract class BaseReplacement> { toString(): string { return `{ ${this.replaceRange.toString()} -> ${this.getNewLength()} }`; } + + get isEmpty() { + return this.getNewLength() === 0 && this.replaceRange.length === 0; + } + + getRangeAfterReplace(): OffsetRange { + return new OffsetRange(this.replaceRange.start, this.replaceRange.start + this.getNewLength()); + } } +export type AnyEdit = BaseEdit; +export type AnyReplacement = BaseReplacement; + export class Edit> extends BaseEdit> { /** * Represents a set of edits to a string. diff --git a/src/vs/editor/common/core/edits/lengthEdit.ts b/src/vs/editor/common/core/edits/lengthEdit.ts index 086c0780c49..bf12178180f 100644 --- a/src/vs/editor/common/core/edits/lengthEdit.ts +++ b/src/vs/editor/common/core/edits/lengthEdit.ts @@ -3,10 +3,47 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { OffsetRange } from '../offsetRange.js'; -import { BaseEdit, BaseReplacement } from './edit.js'; +import { OffsetRange } from '../ranges/offsetRange.js'; +import { AnyEdit, BaseEdit, BaseReplacement } from './edit.js'; +/** + * Like a normal edit, but only captures the length information. +*/ export class LengthEdit extends BaseEdit { + public static readonly empty = new LengthEdit([]); + + public static fromEdit(edit: AnyEdit): LengthEdit { + return new LengthEdit(edit.replacements.map(r => new LengthReplacement(r.replaceRange, r.getNewLength()))); + } + + public static create(replacements: readonly LengthReplacement[]): LengthEdit { + return new LengthEdit(replacements); + } + + public static single(replacement: LengthReplacement): LengthEdit { + return new LengthEdit([replacement]); + } + + public static replace(range: OffsetRange, newLength: number): LengthEdit { + return new LengthEdit([new LengthReplacement(range, newLength)]); + } + + public static insert(offset: number, newLength: number): LengthEdit { + return new LengthEdit([new LengthReplacement(OffsetRange.emptyAt(offset), newLength)]); + } + + public static delete(range: OffsetRange): LengthEdit { + return new LengthEdit([new LengthReplacement(range, 0)]); + } + + public static compose(edits: readonly LengthEdit[]): LengthEdit { + let e = LengthEdit.empty; + for (const edit of edits) { + e = e.compose(edit); + } + return e; + } + /** * Creates an edit that reverts this edit. */ @@ -26,9 +63,46 @@ export class LengthEdit extends BaseEdit { protected override _createNew(replacements: readonly LengthReplacement[]): LengthEdit { return new LengthEdit(replacements); } + + public applyArray(arr: readonly T[], fillItem: T): T[] { + const newArr = new Array(this.getNewDataLength(arr.length)); + + let srcPos = 0; + let dstPos = 0; + + for (const replacement of this.replacements) { + // Copy items before the current replacement + for (let i = srcPos; i < replacement.replaceRange.start; i++) { + newArr[dstPos++] = arr[i]; + } + + // Skip the replaced items in the source array + srcPos = replacement.replaceRange.endExclusive; + + // Fill with the provided fillItem for insertions + for (let i = 0; i < replacement.newLength; i++) { + newArr[dstPos++] = fillItem; + } + } + + // Copy any remaining items from the original array + while (srcPos < arr.length) { + newArr[dstPos++] = arr[srcPos++]; + } + + return newArr; + } } export class LengthReplacement extends BaseReplacement { + public static create( + startOffset: number, + endOffsetExclusive: number, + newLength: number, + ): LengthReplacement { + return new LengthReplacement(new OffsetRange(startOffset, endOffsetExclusive), newLength); + } + constructor( range: OffsetRange, public readonly newLength: number, @@ -49,4 +123,8 @@ export class LengthReplacement extends BaseReplacement { slice(range: OffsetRange, rangeInReplacement: OffsetRange): LengthReplacement { return new LengthReplacement(range, rangeInReplacement.length); } + + override toString() { + return `[${this.replaceRange.start}, +${this.replaceRange.length}) -> +${this.newLength}}`; + } } diff --git a/src/vs/editor/common/core/edits/lineEdit.ts b/src/vs/editor/common/core/edits/lineEdit.ts index c2018a1f106..c7dd4b6dd55 100644 --- a/src/vs/editor/common/core/edits/lineEdit.ts +++ b/src/vs/editor/common/core/edits/lineEdit.ts @@ -7,10 +7,11 @@ import { compareBy, groupAdjacentBy, numberComparator } from '../../../../base/c import { assert, checkAdjacentItems } from '../../../../base/common/assert.js'; import { splitLines } from '../../../../base/common/strings.js'; import { LineRange } from '../ranges/lineRange.js'; -import { OffsetEdit, SingleOffsetEdit } from './offsetEdit.js'; +import { StringEdit, StringReplacement } from './stringEdit.js'; import { Position } from '../position.js'; import { Range } from '../range.js'; -import { AbstractText, SingleTextEdit, TextEdit } from './textEdit.js'; +import { TextReplacement, TextEdit } from './textEdit.js'; +import { AbstractText } from '../text/abstractText.js'; export class LineEdit { public static readonly empty = new LineEdit([]); @@ -19,17 +20,17 @@ export class LineEdit { return new LineEdit(data.map(e => SingleLineEdit.deserialize(e))); } - public static fromEdit(edit: OffsetEdit, initialValue: AbstractText): LineEdit { - const textEdit = TextEdit.fromOffsetEdit(edit, initialValue); + public static fromEdit(edit: StringEdit, initialValue: AbstractText): LineEdit { + const textEdit = TextEdit.fromStringEdit(edit, initialValue); return LineEdit.fromTextEdit(textEdit, initialValue); } public static fromTextEdit(edit: TextEdit, initialValue: AbstractText): LineEdit { - const edits = edit.edits; + const edits = edit.replacements; const result: SingleLineEdit[] = []; - const currentEdits: SingleTextEdit[] = []; + const currentEdits: TextReplacement[] = []; for (let i = 0; i < edits.length; i++) { const edit = edits[i]; const nextEditRange = i + 1 < edits.length ? edits[i + 1] : undefined; @@ -38,7 +39,7 @@ export class LineEdit { continue; } - const singleEdit = SingleTextEdit.joinEdits(currentEdits, initialValue); + const singleEdit = TextReplacement.joinReplacements(currentEdits, initialValue); currentEdits.length = 0; const singleLineEdit = SingleLineEdit.fromSingleTextEdit(singleEdit, initialValue); @@ -63,13 +64,13 @@ export class LineEdit { assert(checkAdjacentItems(edits, (i1, i2) => i1.lineRange.endLineNumberExclusive <= i2.lineRange.startLineNumber)); } - public toEdit(initialValue: AbstractText): OffsetEdit { - const edits: SingleOffsetEdit[] = []; + public toEdit(initialValue: AbstractText): StringEdit { + const edits: StringReplacement[] = []; for (const edit of this.edits) { const singleEdit = edit.toSingleEdit(initialValue); edits.push(singleEdit); } - return new OffsetEdit(edits); + return new StringEdit(edits); } public toString(): string { @@ -215,7 +216,7 @@ export class SingleLineEdit { ); } - public static fromSingleTextEdit(edit: SingleTextEdit, initialValue: AbstractText): SingleLineEdit { + public static fromSingleTextEdit(edit: TextReplacement, initialValue: AbstractText): SingleLineEdit { // 1: ab[cde // 2: fghijk // 3: lmn]opq @@ -269,7 +270,7 @@ export class SingleLineEdit { public readonly newLines: readonly string[], ) { } - public toSingleTextEdit(initialValue: AbstractText): SingleTextEdit { + public toSingleTextEdit(initialValue: AbstractText): TextReplacement { if (this.newLines.length === 0) { // Deletion const textLen = initialValue.getTransformer().textLength; @@ -287,9 +288,9 @@ export class SingleLineEdit { } const lastPosition = textLen.addToPosition(new Position(1, 1)); - return new SingleTextEdit(Range.fromPositions(startPos, lastPosition), ''); + return new TextReplacement(Range.fromPositions(startPos, lastPosition), ''); } else { - return new SingleTextEdit(new Range(this.lineRange.startLineNumber, 1, this.lineRange.endLineNumberExclusive, 1), ''); + return new TextReplacement(new Range(this.lineRange.startLineNumber, 1, this.lineRange.endLineNumberExclusive, 1), ''); } } else if (this.lineRange.isEmpty) { @@ -308,7 +309,7 @@ export class SingleLineEdit { column = 1; text = this.newLines.map(l => l + '\n').join(''); } - return new SingleTextEdit(Range.fromPositions(new Position(endLineNumber, column)), text); + return new TextReplacement(Range.fromPositions(new Position(endLineNumber, column)), text); } else { const endLineNumber = this.lineRange.endLineNumberExclusive - 1; const endLineNumberMaxColumn = initialValue.getTransformer().getLineLength(endLineNumber) + 1; @@ -320,14 +321,14 @@ export class SingleLineEdit { ); // Don't add \n to the last line. This is because we subtract one from lineRange.endLineNumberExclusive for endLineNumber. const text = this.newLines.join('\n'); - return new SingleTextEdit(range, text); + return new TextReplacement(range, text); } } - public toSingleEdit(initialValue: AbstractText): SingleOffsetEdit { + public toSingleEdit(initialValue: AbstractText): StringReplacement { const textEdit = this.toSingleTextEdit(initialValue); const range = initialValue.getTransformer().getOffsetRange(textEdit.range); - return new SingleOffsetEdit(range, textEdit.text); + return new StringReplacement(range, textEdit.text); } public toString(): string { diff --git a/src/vs/editor/common/core/edits/offsetEdit.ts b/src/vs/editor/common/core/edits/offsetEdit.ts deleted file mode 100644 index eb0b4014eca..00000000000 --- a/src/vs/editor/common/core/edits/offsetEdit.ts +++ /dev/null @@ -1,428 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { BugIndicatingError } from '../../../../base/common/errors.js'; -import { OffsetRange } from '../offsetRange.js'; - -/** - * Describes an edit to a (0-based) string. - * Use `TextEdit` to describe edits for a 1-based line/column text. -*/ -export class OffsetEdit { - public static join(edits: readonly OffsetEdit[]): OffsetEdit { - if (edits.length === 0) { - return OffsetEdit.empty; - } - let result = edits[0]; - for (let i = 1; i < edits.length; i++) { - result = result.compose(edits[i]); - } - return result; - } - - public static readonly empty = new OffsetEdit([]); - - public static fromJson(data: IOffsetEdit): OffsetEdit { - return new OffsetEdit(data.map(SingleOffsetEdit.fromJson)); - } - - public static replace(range: OffsetRange, newText: string): OffsetEdit { - return new OffsetEdit([new SingleOffsetEdit(range, newText)]); - } - - public static insert(offset: number, insertText: string): OffsetEdit { - return OffsetEdit.replace(OffsetRange.emptyAt(offset), insertText); - } - - constructor( - public readonly edits: readonly SingleOffsetEdit[], - ) { - let lastEndEx = -1; - for (const edit of edits) { - if (!(edit.replaceRange.start >= lastEndEx)) { - throw new BugIndicatingError(`Edits must be disjoint and sorted. Found ${edit} after ${lastEndEx}`); - } - lastEndEx = edit.replaceRange.endExclusive; - } - } - - normalize(): OffsetEdit { - const edits: SingleOffsetEdit[] = []; - let lastEdit: SingleOffsetEdit | undefined; - for (const edit of this.edits) { - if (edit.newText.length === 0 && edit.replaceRange.length === 0) { - continue; - } - if (lastEdit && lastEdit.replaceRange.endExclusive === edit.replaceRange.start) { - lastEdit = new SingleOffsetEdit( - lastEdit.replaceRange.join(edit.replaceRange), - lastEdit.newText + edit.newText, - ); - } else { - if (lastEdit) { - edits.push(lastEdit); - } - lastEdit = edit; - } - } - if (lastEdit) { - edits.push(lastEdit); - } - return new OffsetEdit(edits); - } - - toString() { - const edits = this.edits.map(e => e.toString()).join(', '); - return `[${edits}]`; - } - - apply(str: string): string { - const resultText: string[] = []; - let pos = 0; - for (const edit of this.edits) { - resultText.push(str.substring(pos, edit.replaceRange.start)); - resultText.push(edit.newText); - pos = edit.replaceRange.endExclusive; - } - resultText.push(str.substring(pos)); - return resultText.join(''); - } - - compose(other: OffsetEdit): OffsetEdit { - return joinEdits(this, other); - } - - /** - * Creates an edit that reverts this edit. - */ - inverse(originalStr: string): OffsetEdit { - const edits: SingleOffsetEdit[] = []; - let offset = 0; - for (const e of this.edits) { - edits.push(new SingleOffsetEdit( - OffsetRange.ofStartAndLength(e.replaceRange.start + offset, e.newText.length), - originalStr.substring(e.replaceRange.start, e.replaceRange.endExclusive), - )); - offset += e.newText.length - e.replaceRange.length; - } - return new OffsetEdit(edits); - } - - getNewTextRanges(): OffsetRange[] { - const ranges: OffsetRange[] = []; - let offset = 0; - for (const e of this.edits) { - ranges.push(OffsetRange.ofStartAndLength(e.replaceRange.start + offset, e.newText.length),); - offset += e.newText.length - e.replaceRange.length; - } - return ranges; - } - - get isEmpty(): boolean { - return this.edits.length === 0; - } - - /** - * Consider `t1 := text o base` and `t2 := text o this`. - * We are interested in `tm := tryMerge(t1, t2, base: text)`. - * For that, we compute `tm' := t1 o base o this.rebase(base)` - * such that `tm' === tm`. - */ - tryRebase(base: OffsetEdit): OffsetEdit; - tryRebase(base: OffsetEdit, noOverlap: true): OffsetEdit | undefined; - tryRebase(base: OffsetEdit, noOverlap?: true): OffsetEdit | undefined { - const newEdits: SingleOffsetEdit[] = []; - - let baseIdx = 0; - let ourIdx = 0; - let offset = 0; - - while (ourIdx < this.edits.length || baseIdx < base.edits.length) { - // take the edit that starts first - const baseEdit = base.edits[baseIdx]; - const ourEdit = this.edits[ourIdx]; - - if (!ourEdit) { - // We processed all our edits - break; - } else if (!baseEdit) { - // no more edits from base - newEdits.push(new SingleOffsetEdit( - ourEdit.replaceRange.delta(offset), - ourEdit.newText, - )); - ourIdx++; - } else if (ourEdit.replaceRange.intersectsOrTouches(baseEdit.replaceRange)) { - ourIdx++; // Don't take our edit, as it is conflicting -> skip - if (noOverlap) { - return undefined; - } - } else if (ourEdit.replaceRange.start < baseEdit.replaceRange.start) { - // Our edit starts first - newEdits.push(new SingleOffsetEdit( - ourEdit.replaceRange.delta(offset), - ourEdit.newText, - )); - ourIdx++; - } else { - baseIdx++; - offset += baseEdit.newText.length - baseEdit.replaceRange.length; - } - } - - return new OffsetEdit(newEdits); - } - - applyToOffset(originalOffset: number): number { - let accumulatedDelta = 0; - for (const edit of this.edits) { - if (edit.replaceRange.start <= originalOffset) { - if (originalOffset < edit.replaceRange.endExclusive) { - // the offset is in the replaced range - return edit.replaceRange.start + accumulatedDelta; - } - accumulatedDelta += edit.newText.length - edit.replaceRange.length; - } else { - break; - } - } - return originalOffset + accumulatedDelta; - } - - applyToOffsetRange(originalRange: OffsetRange): OffsetRange { - return new OffsetRange( - this.applyToOffset(originalRange.start), - this.applyToOffset(originalRange.endExclusive) - ); - } - - applyInverseToOffset(postEditsOffset: number): number { - let accumulatedDelta = 0; - for (const edit of this.edits) { - const editLength = edit.newText.length; - if (edit.replaceRange.start <= postEditsOffset - accumulatedDelta) { - if (postEditsOffset - accumulatedDelta < edit.replaceRange.start + editLength) { - // the offset is in the replaced range - return edit.replaceRange.start; - } - accumulatedDelta += editLength - edit.replaceRange.length; - } else { - break; - } - } - return postEditsOffset - accumulatedDelta; - } - - equals(other: OffsetEdit): boolean { - if (this.edits.length !== other.edits.length) { - return false; - } - for (let i = 0; i < this.edits.length; i++) { - if (!this.edits[i].equals(other.edits[i])) { - return false; - } - - } - return true; - } -} - -export type IOffsetEdit = ISingleOffsetEdit[]; - -export interface ISingleOffsetEdit { - txt: string; - pos: number; - len: number; -} - -export class SingleOffsetEdit { - public static fromJson(data: ISingleOffsetEdit): SingleOffsetEdit { - return new SingleOffsetEdit(OffsetRange.ofStartAndLength(data.pos, data.len), data.txt); - } - - public static insert(offset: number, text: string): SingleOffsetEdit { - return new SingleOffsetEdit(OffsetRange.emptyAt(offset), text); - } - - public static replace(range: OffsetRange, text: string): SingleOffsetEdit { - return new SingleOffsetEdit(range, text); - } - - constructor( - public readonly replaceRange: OffsetRange, - public readonly newText: string, - ) { } - - toString(): string { - return `${this.replaceRange} -> "${this.newText}"`; - } - - get isEmpty() { - return this.newText.length === 0 && this.replaceRange.length === 0; - } - - apply(str: string): string { - return str.substring(0, this.replaceRange.start) + this.newText + str.substring(this.replaceRange.endExclusive); - } - - getRangeAfterApply(): OffsetRange { - return new OffsetRange(this.replaceRange.start, this.replaceRange.start + this.newText.length); - } - - equals(other: SingleOffsetEdit): boolean { - return this.replaceRange.equals(other.replaceRange) && this.newText === other.newText; - } -} - -/** - * Invariant: - * ``` - * edits2.apply(edits1.apply(str)) = join(edits1, edits2).apply(str) - * ``` - */ -function joinEdits(edits1: OffsetEdit, edits2: OffsetEdit): OffsetEdit { - edits1 = edits1.normalize(); - edits2 = edits2.normalize(); - - if (edits1.isEmpty) { return edits2; } - if (edits2.isEmpty) { return edits1; } - - const edit1Queue = [...edits1.edits]; - const result: SingleOffsetEdit[] = []; - - let edit1ToEdit2 = 0; - - for (const edit2 of edits2.edits) { - // Copy over edit1 unmodified until it touches edit2. - while (true) { - const edit1 = edit1Queue[0]!; - if (!edit1 || edit1.replaceRange.start + edit1ToEdit2 + edit1.newText.length >= edit2.replaceRange.start) { - break; - } - edit1Queue.shift(); - - result.push(edit1); - edit1ToEdit2 += edit1.newText.length - edit1.replaceRange.length; - } - - const firstEdit1ToEdit2 = edit1ToEdit2; - let firstIntersecting: SingleOffsetEdit | undefined; // or touching - let lastIntersecting: SingleOffsetEdit | undefined; // or touching - - while (true) { - const edit1 = edit1Queue[0]; - if (!edit1 || edit1.replaceRange.start + edit1ToEdit2 > edit2.replaceRange.endExclusive) { - break; - } - // else we intersect, because the new end of edit1 is after or equal to our start - - if (!firstIntersecting) { - firstIntersecting = edit1; - } - lastIntersecting = edit1; - edit1Queue.shift(); - - edit1ToEdit2 += edit1.newText.length - edit1.replaceRange.length; - } - - if (!firstIntersecting) { - result.push(new SingleOffsetEdit(edit2.replaceRange.delta(-edit1ToEdit2), edit2.newText)); - } else { - let prefix = ''; - const prefixLength = edit2.replaceRange.start - (firstIntersecting.replaceRange.start + firstEdit1ToEdit2); - if (prefixLength > 0) { - prefix = firstIntersecting.newText.slice(0, prefixLength); - } - const suffixLength = (lastIntersecting!.replaceRange.endExclusive + edit1ToEdit2) - edit2.replaceRange.endExclusive; - if (suffixLength > 0) { - const e = new SingleOffsetEdit(OffsetRange.ofStartAndLength(lastIntersecting!.replaceRange.endExclusive, 0), lastIntersecting!.newText.slice(-suffixLength)); - edit1Queue.unshift(e); - edit1ToEdit2 -= e.newText.length - e.replaceRange.length; - } - const newText = prefix + edit2.newText; - - const newReplaceRange = new OffsetRange( - Math.min(firstIntersecting.replaceRange.start, edit2.replaceRange.start - firstEdit1ToEdit2), - edit2.replaceRange.endExclusive - edit1ToEdit2 - ); - result.push(new SingleOffsetEdit(newReplaceRange, newText)); - } - } - - while (true) { - const item = edit1Queue.shift(); - if (!item) { break; } - result.push(item); - } - - return new OffsetEdit(result).normalize(); -} - -export function applyEditsToRanges(sortedRanges: OffsetRange[], edits: OffsetEdit): OffsetRange[] { - sortedRanges = sortedRanges.slice(); - - // treat edits as deletion of the replace range and then as insertion that extends the first range - const result: OffsetRange[] = []; - - let offset = 0; - - for (const e of edits.edits) { - while (true) { - // ranges before the current edit - const r = sortedRanges[0]; - if (!r || r.endExclusive >= e.replaceRange.start) { - break; - } - sortedRanges.shift(); - result.push(r.delta(offset)); - } - - const intersecting: OffsetRange[] = []; - while (true) { - const r = sortedRanges[0]; - if (!r || !r.intersectsOrTouches(e.replaceRange)) { - break; - } - sortedRanges.shift(); - intersecting.push(r); - } - - for (let i = intersecting.length - 1; i >= 0; i--) { - let r = intersecting[i]; - - const overlap = r.intersect(e.replaceRange)!.length; - r = r.deltaEnd(-overlap + (i === 0 ? e.newText.length : 0)); - - const rangeAheadOfReplaceRange = r.start - e.replaceRange.start; - if (rangeAheadOfReplaceRange > 0) { - r = r.delta(-rangeAheadOfReplaceRange); - } - - if (i !== 0) { - r = r.delta(e.newText.length); - } - - // We already took our offset into account. - // Because we add r back to the queue (which then adds offset again), - // we have to remove it here. - r = r.delta(-(e.newText.length - e.replaceRange.length)); - - sortedRanges.unshift(r); - } - - offset += e.newText.length - e.replaceRange.length; - } - - while (true) { - const r = sortedRanges[0]; - if (!r) { - break; - } - sortedRanges.shift(); - result.push(r.delta(offset)); - } - - return result; -} diff --git a/src/vs/editor/common/core/edits/stringEdit.ts b/src/vs/editor/common/core/edits/stringEdit.ts index af00c31dd1d..56d91fdb4d8 100644 --- a/src/vs/editor/common/core/edits/stringEdit.ts +++ b/src/vs/editor/common/core/edits/stringEdit.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { OffsetRange } from '../offsetRange.js'; +import { OffsetRange } from '../ranges/offsetRange.js'; import { BaseEdit, BaseReplacement } from './edit.js'; /** @@ -33,6 +33,25 @@ export class StringEdit extends BaseEdit { return new StringEdit([new StringReplacement(range, '')]); } + public static fromJson(data: ISerializedStringEdit): StringEdit { + return new StringEdit(data.map(StringReplacement.fromJson)); + } + + public static compose(edits: readonly StringEdit[]): StringEdit { + if (edits.length === 0) { + return StringEdit.empty; + } + let result = edits[0]; + for (let i = 1; i < edits.length; i++) { + result = result.compose(edits[i]); + } + return result; + } + + constructor(replacements: readonly StringReplacement[]) { + super(replacements); + } + protected override _createNew(replacements: readonly StringReplacement[]): StringEdit { return new StringEdit(replacements); } @@ -42,7 +61,7 @@ export class StringEdit extends BaseEdit { let pos = 0; for (const edit of this.replacements) { resultText.push(base.substring(pos, edit.replaceRange.start)); - resultText.push(edit.newValue); + resultText.push(edit.newText); pos = edit.replaceRange.endExclusive; } resultText.push(base.substring(pos)); @@ -57,34 +76,194 @@ export class StringEdit extends BaseEdit { let offset = 0; for (const e of this.replacements) { edits.push(new StringReplacement( - OffsetRange.ofStartAndLength(e.replaceRange.start + offset, e.newValue.length), + OffsetRange.ofStartAndLength(e.replaceRange.start + offset, e.newText.length), baseStr.substring(e.replaceRange.start, e.replaceRange.endExclusive), )); - offset += e.newValue.length - e.replaceRange.length; + offset += e.newText.length - e.replaceRange.length; } return new StringEdit(edits); } + + /** + * Consider `t1 := text o base` and `t2 := text o this`. + * We are interested in `tm := tryMerge(t1, t2, base: text)`. + * For that, we compute `tm' := t1 o base o this.rebase(base)` + * such that `tm' === tm`. + */ + public tryRebase(base: StringEdit): StringEdit; + public tryRebase(base: StringEdit, noOverlap: true): StringEdit | undefined; + public tryRebase(base: StringEdit, noOverlap?: true): StringEdit | undefined { + const newEdits: StringReplacement[] = []; + + let baseIdx = 0; + let ourIdx = 0; + let offset = 0; + + while (ourIdx < this.replacements.length || baseIdx < base.replacements.length) { + // take the edit that starts first + const baseEdit = base.replacements[baseIdx]; + const ourEdit = this.replacements[ourIdx]; + + if (!ourEdit) { + // We processed all our edits + break; + } else if (!baseEdit) { + // no more edits from base + newEdits.push(new StringReplacement( + ourEdit.replaceRange.delta(offset), + ourEdit.newText, + )); + ourIdx++; + } else if (ourEdit.replaceRange.intersectsOrTouches(baseEdit.replaceRange)) { + ourIdx++; // Don't take our edit, as it is conflicting -> skip + if (noOverlap) { + return undefined; + } + } else if (ourEdit.replaceRange.start < baseEdit.replaceRange.start) { + // Our edit starts first + newEdits.push(new StringReplacement( + ourEdit.replaceRange.delta(offset), + ourEdit.newText, + )); + ourIdx++; + } else { + baseIdx++; + offset += baseEdit.newText.length - baseEdit.replaceRange.length; + } + } + + return new StringEdit(newEdits); + } + + public toJson(): ISerializedStringEdit { + return this.replacements.map(e => ({ + txt: e.newText, + pos: e.replaceRange.start, + len: e.replaceRange.length, + })); + } +} + +/** + * Warning: Be careful when changing this type, as it is used for serialization! +*/ +export type ISerializedStringEdit = ISerializedStringReplacement[]; + +/** + * Warning: Be careful when changing this type, as it is used for serialization! +*/ +export interface ISerializedStringReplacement { + txt: string; + pos: number; + len: number; } export class StringReplacement extends BaseReplacement { + public static insert(offset: number, text: string): StringReplacement { + return new StringReplacement(OffsetRange.emptyAt(offset), text); + } + + public static replace(range: OffsetRange, text: string): StringReplacement { + return new StringReplacement(range, text); + } + + public static fromJson(data: ISerializedStringReplacement): StringReplacement { + return new StringReplacement(OffsetRange.ofStartAndLength(data.pos, data.len), data.txt); + } + constructor( range: OffsetRange, - public readonly newValue: string, + public readonly newText: string, ) { super(range); } override equals(other: StringReplacement): boolean { - return this.replaceRange.equals(other.replaceRange) && this.newValue === other.newValue; + return this.replaceRange.equals(other.replaceRange) && this.newText === other.newText; } - getNewLength(): number { return this.newValue.length; } + getNewLength(): number { return this.newText.length; } tryJoinTouching(other: StringReplacement): StringReplacement | undefined { - return new StringReplacement(this.replaceRange.joinRightTouching(other.replaceRange), this.newValue + other.newValue); + return new StringReplacement(this.replaceRange.joinRightTouching(other.replaceRange), this.newText + other.newText); } slice(range: OffsetRange, rangeInReplacement: OffsetRange): StringReplacement { - return new StringReplacement(range, rangeInReplacement.substring(this.newValue)); + return new StringReplacement(range, rangeInReplacement.substring(this.newText)); + } + + override toString(): string { + return `${this.replaceRange} -> "${this.newText}"`; + } + + replace(str: string): string { + return str.substring(0, this.replaceRange.start) + this.newText + str.substring(this.replaceRange.endExclusive); } } + +export function applyEditsToRanges(sortedRanges: OffsetRange[], edit: StringEdit): OffsetRange[] { + sortedRanges = sortedRanges.slice(); + + // treat edits as deletion of the replace range and then as insertion that extends the first range + const result: OffsetRange[] = []; + + let offset = 0; + + for (const e of edit.replacements) { + while (true) { + // ranges before the current edit + const r = sortedRanges[0]; + if (!r || r.endExclusive >= e.replaceRange.start) { + break; + } + sortedRanges.shift(); + result.push(r.delta(offset)); + } + + const intersecting: OffsetRange[] = []; + while (true) { + const r = sortedRanges[0]; + if (!r || !r.intersectsOrTouches(e.replaceRange)) { + break; + } + sortedRanges.shift(); + intersecting.push(r); + } + + for (let i = intersecting.length - 1; i >= 0; i--) { + let r = intersecting[i]; + + const overlap = r.intersect(e.replaceRange)!.length; + r = r.deltaEnd(-overlap + (i === 0 ? e.newText.length : 0)); + + const rangeAheadOfReplaceRange = r.start - e.replaceRange.start; + if (rangeAheadOfReplaceRange > 0) { + r = r.delta(-rangeAheadOfReplaceRange); + } + + if (i !== 0) { + r = r.delta(e.newText.length); + } + + // We already took our offset into account. + // Because we add r back to the queue (which then adds offset again), + // we have to remove it here. + r = r.delta(-(e.newText.length - e.replaceRange.length)); + + sortedRanges.unshift(r); + } + + offset += e.newText.length - e.replaceRange.length; + } + + while (true) { + const r = sortedRanges[0]; + if (!r) { + break; + } + sortedRanges.shift(); + result.push(r.delta(offset)); + } + + return result; +} diff --git a/src/vs/editor/common/core/edits/textEdit.ts b/src/vs/editor/common/core/edits/textEdit.ts index 278a7f80b19..6082a0f0298 100644 --- a/src/vs/editor/common/core/edits/textEdit.ts +++ b/src/vs/editor/common/core/edits/textEdit.ts @@ -4,49 +4,50 @@ *--------------------------------------------------------------------------------------------*/ import { equals } from '../../../../base/common/arrays.js'; -import { assert, assertFn, checkAdjacentItems } from '../../../../base/common/assert.js'; +import { assertFn, checkAdjacentItems } from '../../../../base/common/assert.js'; import { BugIndicatingError } from '../../../../base/common/errors.js'; -import { commonPrefixLength, commonSuffixLength, splitLines } from '../../../../base/common/strings.js'; +import { commonPrefixLength, commonSuffixLength } from '../../../../base/common/strings.js'; import { ISingleEditOperation } from '../editOperation.js'; -import { LineRange } from '../ranges/lineRange.js'; -import { OffsetEdit } from './offsetEdit.js'; +import { StringEdit } from './stringEdit.js'; import { Position } from '../position.js'; -import { PositionOffsetTransformer } from '../positionToOffset.js'; import { Range } from '../range.js'; -import { TextLength } from '../textLength.js'; +import { TextLength } from '../text/textLength.js'; +import { AbstractText, StringText } from '../text/abstractText.js'; export class TextEdit { - public static fromOffsetEdit(edit: OffsetEdit, initialState: AbstractText): TextEdit { - const edits = edit.edits.map(e => new SingleTextEdit(initialState.getTransformer().getRange(e.replaceRange), e.newText)); + public static fromStringEdit(edit: StringEdit, initialState: AbstractText): TextEdit { + const edits = edit.replacements.map(e => new TextReplacement(initialState.getTransformer().getRange(e.replaceRange), e.newText)); return new TextEdit(edits); } - public static single(originalRange: Range, newText: string): TextEdit { - return new TextEdit([new SingleTextEdit(originalRange, newText)]); + public static replace(originalRange: Range, newText: string): TextEdit { + return new TextEdit([new TextReplacement(originalRange, newText)]); } public static insert(position: Position, newText: string): TextEdit { - return new TextEdit([new SingleTextEdit(Range.fromPositions(position, position), newText)]); + return new TextEdit([new TextReplacement(Range.fromPositions(position, position), newText)]); } - constructor(public readonly edits: readonly SingleTextEdit[]) { - assertFn(() => checkAdjacentItems(edits, (a, b) => a.range.getEndPosition().isBeforeOrEqual(b.range.getStartPosition()))); + constructor( + public readonly replacements: readonly TextReplacement[] + ) { + assertFn(() => checkAdjacentItems(replacements, (a, b) => a.range.getEndPosition().isBeforeOrEqual(b.range.getStartPosition()))); } /** * Joins touching edits and removes empty edits. */ normalize(): TextEdit { - const edits: SingleTextEdit[] = []; - for (const edit of this.edits) { - if (edits.length > 0 && edits[edits.length - 1].range.getEndPosition().equals(edit.range.getStartPosition())) { - const last = edits[edits.length - 1]; - edits[edits.length - 1] = new SingleTextEdit(last.range.plusRange(edit.range), last.text + edit.text); - } else if (!edit.isEmpty) { - edits.push(edit); + const replacements: TextReplacement[] = []; + for (const r of this.replacements) { + if (replacements.length > 0 && replacements[replacements.length - 1].range.getEndPosition().equals(r.range.getStartPosition())) { + const last = replacements[replacements.length - 1]; + replacements[replacements.length - 1] = new TextReplacement(last.range.plusRange(r.range), last.text + r.text); + } else if (!r.isEmpty) { + replacements.push(r); } } - return new TextEdit(edits); + return new TextEdit(replacements); } mapPosition(position: Position): Position | Range { @@ -54,15 +55,15 @@ export class TextEdit { let curLine = 0; let columnDeltaInCurLine = 0; - for (const edit of this.edits) { - const start = edit.range.getStartPosition(); + for (const replacement of this.replacements) { + const start = replacement.range.getStartPosition(); if (position.isBeforeOrEqual(start)) { break; } - const end = edit.range.getEndPosition(); - const len = TextLength.ofText(edit.text); + const end = replacement.range.getEndPosition(); + const len = TextLength.ofText(replacement.text); if (position.isBefore(end)) { const startPos = new Position(start.lineNumber + lineDelta, start.column + (start.lineNumber + lineDelta === curLine ? columnDeltaInCurLine : 0)); const endPos = len.addToPosition(startPos); @@ -73,7 +74,7 @@ export class TextEdit { columnDeltaInCurLine = 0; } - lineDelta += len.lineCount - (edit.range.endLineNumber - edit.range.startLineNumber); + lineDelta += len.lineCount - (replacement.range.endLineNumber - replacement.range.startLineNumber); if (len.lineCount === 0) { if (end.lineNumber !== start.lineNumber) { @@ -119,8 +120,8 @@ export class TextEdit { apply(text: AbstractText): string { let result = ''; let lastEditEnd = new Position(1, 1); - for (const edit of this.edits) { - const editRange = edit.range; + for (const replacement of this.replacements) { + const editRange = replacement.range; const editStart = editRange.getStartPosition(); const editEnd = editRange.getEndPosition(); @@ -128,7 +129,7 @@ export class TextEdit { if (!r.isEmpty()) { result += text.getValueOfRange(r); } - result += edit.text; + result += replacement.text; lastEditEnd = editEnd; } const r = rangeFromPositions(lastEditEnd, text.endPositionExclusive); @@ -145,7 +146,7 @@ export class TextEdit { inverse(doc: AbstractText): TextEdit { const ranges = this.getNewRanges(); - return new TextEdit(this.edits.map((e, idx) => new SingleTextEdit(ranges[idx], doc.getValueOfRange(e.range)))); + return new TextEdit(this.replacements.map((e, idx) => new TextReplacement(ranges[idx], doc.getValueOfRange(e.range)))); } getNewRanges(): Range[] { @@ -153,68 +154,68 @@ export class TextEdit { let previousEditEndLineNumber = 0; let lineOffset = 0; let columnOffset = 0; - for (const edit of this.edits) { - const textLength = TextLength.ofText(edit.text); + for (const replacement of this.replacements) { + const textLength = TextLength.ofText(replacement.text); const newRangeStart = Position.lift({ - lineNumber: edit.range.startLineNumber + lineOffset, - column: edit.range.startColumn + (edit.range.startLineNumber === previousEditEndLineNumber ? columnOffset : 0) + lineNumber: replacement.range.startLineNumber + lineOffset, + column: replacement.range.startColumn + (replacement.range.startLineNumber === previousEditEndLineNumber ? columnOffset : 0) }); const newRange = textLength.createRange(newRangeStart); newRanges.push(newRange); - lineOffset = newRange.endLineNumber - edit.range.endLineNumber; - columnOffset = newRange.endColumn - edit.range.endColumn; - previousEditEndLineNumber = edit.range.endLineNumber; + lineOffset = newRange.endLineNumber - replacement.range.endLineNumber; + columnOffset = newRange.endColumn - replacement.range.endColumn; + previousEditEndLineNumber = replacement.range.endLineNumber; } return newRanges; } - toSingle(text: AbstractText) { - if (this.edits.length === 0) { throw new BugIndicatingError(); } - if (this.edits.length === 1) { return this.edits[0]; } + toReplacement(text: AbstractText): TextReplacement { + if (this.replacements.length === 0) { throw new BugIndicatingError(); } + if (this.replacements.length === 1) { return this.replacements[0]; } - const startPos = this.edits[0].range.getStartPosition(); - const endPos = this.edits[this.edits.length - 1].range.getEndPosition(); + const startPos = this.replacements[0].range.getStartPosition(); + const endPos = this.replacements[this.replacements.length - 1].range.getEndPosition(); let newText = ''; - for (let i = 0; i < this.edits.length; i++) { - const curEdit = this.edits[i]; + for (let i = 0; i < this.replacements.length; i++) { + const curEdit = this.replacements[i]; newText += curEdit.text; - if (i < this.edits.length - 1) { - const nextEdit = this.edits[i + 1]; + if (i < this.replacements.length - 1) { + const nextEdit = this.replacements[i + 1]; const gapRange = Range.fromPositions(curEdit.range.getEndPosition(), nextEdit.range.getStartPosition()); const gapText = text.getValueOfRange(gapRange); newText += gapText; } } - return new SingleTextEdit(Range.fromPositions(startPos, endPos), newText); + return new TextReplacement(Range.fromPositions(startPos, endPos), newText); } equals(other: TextEdit): boolean { - return equals(this.edits, other.edits, (a, b) => a.equals(b)); + return equals(this.replacements, other.replacements, (a, b) => a.equals(b)); } toString(text: AbstractText | string | undefined): string { if (text === undefined) { - return this.edits.map(edit => edit.toString()).join('\n'); + return this.replacements.map(edit => edit.toString()).join('\n'); } if (typeof text === 'string') { return this.toString(new StringText(text)); } - if (this.edits.length === 0) { + if (this.replacements.length === 0) { return ''; } - return this.edits.map(edit => { + return this.replacements.map(r => { const maxLength = 10; - const originalText = text.getValueOfRange(edit.range); + const originalText = text.getValueOfRange(r.range); // Get text before the edit const beforeRange = Range.fromPositions( - new Position(Math.max(1, edit.range.startLineNumber - 1), 1), - edit.range.getStartPosition() + new Position(Math.max(1, r.range.startLineNumber - 1), 1), + r.range.getStartPosition() ); let beforeText = text.getValueOfRange(beforeRange); if (beforeText.length > maxLength) { @@ -223,8 +224,8 @@ export class TextEdit { // Get text after the edit const afterRange = Range.fromPositions( - edit.range.getEndPosition(), - new Position(edit.range.endLineNumber + 1, 1) + r.range.getEndPosition(), + new Position(r.range.endLineNumber + 1, 1) ); let afterText = text.getValueOfRange(afterRange); if (afterText.length > maxLength) { @@ -240,7 +241,7 @@ export class TextEdit { } // Format the new text - let newText = edit.text; + let newText = r.text; if (newText.length > maxLength) { const halfMax = Math.floor(maxLength / 2); newText = newText.substring(0, halfMax) + '...' + @@ -257,27 +258,27 @@ export class TextEdit { } } -export class SingleTextEdit { - public static joinEdits(edits: SingleTextEdit[], initialValue: AbstractText): SingleTextEdit { - if (edits.length === 0) { throw new BugIndicatingError(); } - if (edits.length === 1) { return edits[0]; } +export class TextReplacement { + public static joinReplacements(replacements: TextReplacement[], initialValue: AbstractText): TextReplacement { + if (replacements.length === 0) { throw new BugIndicatingError(); } + if (replacements.length === 1) { return replacements[0]; } - const startPos = edits[0].range.getStartPosition(); - const endPos = edits[edits.length - 1].range.getEndPosition(); + const startPos = replacements[0].range.getStartPosition(); + const endPos = replacements[replacements.length - 1].range.getEndPosition(); let newText = ''; - for (let i = 0; i < edits.length; i++) { - const curEdit = edits[i]; + for (let i = 0; i < replacements.length; i++) { + const curEdit = replacements[i]; newText += curEdit.text; - if (i < edits.length - 1) { - const nextEdit = edits[i + 1]; + if (i < replacements.length - 1) { + const nextEdit = replacements[i + 1]; const gapRange = Range.fromPositions(curEdit.range.getEndPosition(), nextEdit.range.getStartPosition()); const gapText = initialValue.getValueOfRange(gapRange); newText += gapText; } } - return new SingleTextEdit(Range.fromPositions(startPos, endPos), newText); + return new TextReplacement(Range.fromPositions(startPos, endPos), newText); } constructor( @@ -290,7 +291,7 @@ export class SingleTextEdit { return this.range.isEmpty() && this.text.length === 0; } - static equals(first: SingleTextEdit, second: SingleTextEdit) { + static equals(first: TextReplacement, second: TextReplacement) { return first.range.equalsRange(second.range) && first.text === second.text; } @@ -305,21 +306,21 @@ export class SingleTextEdit { return new TextEdit([this]); } - public equals(other: SingleTextEdit): boolean { - return SingleTextEdit.equals(this, other); + public equals(other: TextReplacement): boolean { + return TextReplacement.equals(this, other); } - public extendToCoverRange(range: Range, initialValue: AbstractText): SingleTextEdit { + public extendToCoverRange(range: Range, initialValue: AbstractText): TextReplacement { if (this.range.containsRange(range)) { return this; } const newRange = this.range.plusRange(range); const textBefore = initialValue.getValueOfRange(Range.fromPositions(newRange.getStartPosition(), this.range.getStartPosition())); const textAfter = initialValue.getValueOfRange(Range.fromPositions(this.range.getEndPosition(), newRange.getEndPosition())); const newText = textBefore + this.text + textAfter; - return new SingleTextEdit(newRange, newText); + return new TextReplacement(newRange, newText); } - public extendToFullLine(initialValue: AbstractText): SingleTextEdit { + public extendToFullLine(initialValue: AbstractText): TextReplacement { const newRange = new Range( this.range.startLineNumber, 1, @@ -329,7 +330,7 @@ export class SingleTextEdit { return this.extendToCoverRange(newRange, initialValue); } - public removeCommonPrefix(text: AbstractText): SingleTextEdit { + public removeCommonPrefix(text: AbstractText): TextReplacement { const normalizedOriginalText = text.getValueOfRange(this.range).replaceAll('\r\n', '\n'); const normalizedModifiedText = this.text.replaceAll('\r\n', '\n'); @@ -339,7 +340,7 @@ export class SingleTextEdit { const newText = normalizedModifiedText.substring(commonPrefixLen); const range = Range.fromPositions(start, this.range.getEndPosition()); - return new SingleTextEdit(range, newText); + return new TextReplacement(range, newText); } public isEffectiveDeletion(text: AbstractText): boolean { @@ -364,100 +365,3 @@ function rangeFromPositions(start: Position, end: Position): Range { } return new Range(start.lineNumber, start.column, end.lineNumber, end.column); } - -export abstract class AbstractText { - abstract getValueOfRange(range: Range): string; - abstract readonly length: TextLength; - - get endPositionExclusive(): Position { - return this.length.addToPosition(new Position(1, 1)); - } - - get lineRange(): LineRange { - return this.length.toLineRange(); - } - - getValue(): string { - return this.getValueOfRange(this.length.toRange()); - } - - getLineLength(lineNumber: number): number { - return this.getValueOfRange(new Range(lineNumber, 1, lineNumber, Number.MAX_SAFE_INTEGER)).length; - } - - private _transformer: PositionOffsetTransformer | undefined = undefined; - - getTransformer(): PositionOffsetTransformer { - if (!this._transformer) { - this._transformer = new PositionOffsetTransformer(this.getValue()); - } - return this._transformer; - } - - getLineAt(lineNumber: number): string { - return this.getValueOfRange(new Range(lineNumber, 1, lineNumber, Number.MAX_SAFE_INTEGER)); - } - - getLines(): string[] { - const value = this.getValue(); - return splitLines(value); - } -} - -export class LineBasedText extends AbstractText { - constructor( - private readonly _getLineContent: (lineNumber: number) => string, - private readonly _lineCount: number, - ) { - assert(_lineCount >= 1); - - super(); - } - - override getValueOfRange(range: Range): string { - if (range.startLineNumber === range.endLineNumber) { - return this._getLineContent(range.startLineNumber).substring(range.startColumn - 1, range.endColumn - 1); - } - let result = this._getLineContent(range.startLineNumber).substring(range.startColumn - 1); - for (let i = range.startLineNumber + 1; i < range.endLineNumber; i++) { - result += '\n' + this._getLineContent(i); - } - result += '\n' + this._getLineContent(range.endLineNumber).substring(0, range.endColumn - 1); - return result; - } - - override getLineLength(lineNumber: number): number { - return this._getLineContent(lineNumber).length; - } - - get length(): TextLength { - const lastLine = this._getLineContent(this._lineCount); - return new TextLength(this._lineCount - 1, lastLine.length); - } -} - -export class ArrayText extends LineBasedText { - constructor(lines: string[]) { - super( - lineNumber => lines[lineNumber - 1], - lines.length - ); - } -} - -export class StringText extends AbstractText { - private readonly _t; - - constructor(public readonly value: string) { - super(); - this._t = new PositionOffsetTransformer(this.value); - } - - getValueOfRange(range: Range): string { - return this._t.getOffsetRange(range).substring(this.value); - } - - get length(): TextLength { - return this._t.textLength; - } -} diff --git a/src/vs/editor/common/core/ranges/columnRange.ts b/src/vs/editor/common/core/ranges/columnRange.ts index fec6b35219a..1907c709538 100644 --- a/src/vs/editor/common/core/ranges/columnRange.ts +++ b/src/vs/editor/common/core/ranges/columnRange.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { BugIndicatingError } from '../../../../base/common/errors.js'; -import { OffsetRange } from '../offsetRange.js'; +import { OffsetRange } from './offsetRange.js'; import { Range } from '../range.js'; /** diff --git a/src/vs/editor/common/core/ranges/lineRange.ts b/src/vs/editor/common/core/ranges/lineRange.ts index 3d0791dfb35..8b62e0927fc 100644 --- a/src/vs/editor/common/core/ranges/lineRange.ts +++ b/src/vs/editor/common/core/ranges/lineRange.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { BugIndicatingError } from '../../../../base/common/errors.js'; -import { OffsetRange } from '../offsetRange.js'; +import { OffsetRange } from './offsetRange.js'; import { Range } from '../range.js'; import { findFirstIdxMonotonousOrArrLen, findLastIdxMonotonous, findLastMonotonous } from '../../../../base/common/arraysFind.js'; import { Comparator, compareBy, numberComparator } from '../../../../base/common/arrays.js'; diff --git a/src/vs/editor/common/core/offsetRange.ts b/src/vs/editor/common/core/ranges/offsetRange.ts similarity index 99% rename from src/vs/editor/common/core/offsetRange.ts rename to src/vs/editor/common/core/ranges/offsetRange.ts index 975dddc1eaf..d776adc4924 100644 --- a/src/vs/editor/common/core/offsetRange.ts +++ b/src/vs/editor/common/core/ranges/offsetRange.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { BugIndicatingError } from '../../../base/common/errors.js'; +import { BugIndicatingError } from '../../../../base/common/errors.js'; export interface IOffsetRange { readonly start: number; diff --git a/src/vs/editor/common/core/ranges/rangeMapping.ts b/src/vs/editor/common/core/ranges/rangeMapping.ts index a6a80bbfc89..b8e9d09337b 100644 --- a/src/vs/editor/common/core/ranges/rangeMapping.ts +++ b/src/vs/editor/common/core/ranges/rangeMapping.ts @@ -6,7 +6,7 @@ import { findLastMonotonous } from '../../../../base/common/arraysFind.js'; import { Position } from '../position.js'; import { Range } from '../range.js'; -import { TextLength } from '../textLength.js'; +import { TextLength } from '../text/textLength.js'; /** * Represents a list of mappings of ranges from one document to another. diff --git a/src/vs/editor/common/core/text/abstractText.ts b/src/vs/editor/common/core/text/abstractText.ts new file mode 100644 index 00000000000..274a05f2e0e --- /dev/null +++ b/src/vs/editor/common/core/text/abstractText.ts @@ -0,0 +1,109 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { assert } from '../../../../base/common/assert.js'; +import { splitLines } from '../../../../base/common/strings.js'; +import { Position } from '../position.js'; +import { PositionOffsetTransformer } from './positionToOffset.js'; +import { Range } from '../range.js'; +import { LineRange } from '../ranges/lineRange.js'; +import { TextLength } from '../text/textLength.js'; + +export abstract class AbstractText { + abstract getValueOfRange(range: Range): string; + abstract readonly length: TextLength; + + get endPositionExclusive(): Position { + return this.length.addToPosition(new Position(1, 1)); + } + + get lineRange(): LineRange { + return this.length.toLineRange(); + } + + getValue(): string { + return this.getValueOfRange(this.length.toRange()); + } + + getLineLength(lineNumber: number): number { + return this.getValueOfRange(new Range(lineNumber, 1, lineNumber, Number.MAX_SAFE_INTEGER)).length; + } + + private _transformer: PositionOffsetTransformer | undefined = undefined; + + getTransformer(): PositionOffsetTransformer { + if (!this._transformer) { + this._transformer = new PositionOffsetTransformer(this.getValue()); + } + return this._transformer; + } + + getLineAt(lineNumber: number): string { + return this.getValueOfRange(new Range(lineNumber, 1, lineNumber, Number.MAX_SAFE_INTEGER)); + } + + getLines(): string[] { + const value = this.getValue(); + return splitLines(value); + } +} + +export class LineBasedText extends AbstractText { + constructor( + private readonly _getLineContent: (lineNumber: number) => string, + private readonly _lineCount: number + ) { + assert(_lineCount >= 1); + + super(); + } + + override getValueOfRange(range: Range): string { + if (range.startLineNumber === range.endLineNumber) { + return this._getLineContent(range.startLineNumber).substring(range.startColumn - 1, range.endColumn - 1); + } + let result = this._getLineContent(range.startLineNumber).substring(range.startColumn - 1); + for (let i = range.startLineNumber + 1; i < range.endLineNumber; i++) { + result += '\n' + this._getLineContent(i); + } + result += '\n' + this._getLineContent(range.endLineNumber).substring(0, range.endColumn - 1); + return result; + } + + override getLineLength(lineNumber: number): number { + return this._getLineContent(lineNumber).length; + } + + get length(): TextLength { + const lastLine = this._getLineContent(this._lineCount); + return new TextLength(this._lineCount - 1, lastLine.length); + } +} + +export class ArrayText extends LineBasedText { + constructor(lines: string[]) { + super( + lineNumber => lines[lineNumber - 1], + lines.length + ); + } +} + +export class StringText extends AbstractText { + private readonly _t; + + constructor(public readonly value: string) { + super(); + this._t = new PositionOffsetTransformer(this.value); + } + + getValueOfRange(range: Range): string { + return this._t.getOffsetRange(range).substring(this.value); + } + + get length(): TextLength { + return this._t.textLength; + } +} diff --git a/src/vs/editor/common/core/positionToOffset.ts b/src/vs/editor/common/core/text/positionToOffset.ts similarity index 78% rename from src/vs/editor/common/core/positionToOffset.ts rename to src/vs/editor/common/core/text/positionToOffset.ts index 96fd066ef29..1f2ea7ded84 100644 --- a/src/vs/editor/common/core/positionToOffset.ts +++ b/src/vs/editor/common/core/text/positionToOffset.ts @@ -3,14 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { findLastIdxMonotonous } from '../../../base/common/arraysFind.js'; -import { ITextModel } from '../model.js'; -import { OffsetEdit, SingleOffsetEdit } from './edits/offsetEdit.js'; -import { OffsetRange } from './offsetRange.js'; -import { Position } from './position.js'; -import { Range } from './range.js'; -import { SingleTextEdit, TextEdit } from './edits/textEdit.js'; -import { TextLength } from './textLength.js'; +import { findLastIdxMonotonous } from '../../../../base/common/arraysFind.js'; +import { ITextModel } from '../../model.js'; +import { StringEdit, StringReplacement } from '../edits/stringEdit.js'; +import { OffsetRange } from '../ranges/offsetRange.js'; +import { Position } from '../position.js'; +import { Range } from '../range.js'; +import { TextReplacement, TextEdit } from '../edits/textEdit.js'; +import { TextLength } from '../text/textLength.js'; export abstract class PositionOffsetTransformerBase { abstract getOffset(position: Position): number; @@ -31,21 +31,21 @@ export abstract class PositionOffsetTransformerBase { ); } - getOffsetEdit(edit: TextEdit): OffsetEdit { - const edits = edit.edits.map(e => this.getSingleOffsetEdit(e)); - return new OffsetEdit(edits); + getStringEdit(edit: TextEdit): StringEdit { + const edits = edit.replacements.map(e => this.getStringReplacement(e)); + return new StringEdit(edits); } - getSingleOffsetEdit(edit: SingleTextEdit): SingleOffsetEdit { - return new SingleOffsetEdit(this.getOffsetRange(edit.range), edit.text); + getStringReplacement(edit: TextReplacement): StringReplacement { + return new StringReplacement(this.getOffsetRange(edit.range), edit.text); } - getSingleTextEdit(edit: SingleOffsetEdit): SingleTextEdit { - return new SingleTextEdit(this.getRange(edit.replaceRange), edit.newText); + getSingleTextEdit(edit: StringReplacement): TextReplacement { + return new TextReplacement(this.getRange(edit.replaceRange), edit.newText); } - getTextEdit(edit: OffsetEdit): TextEdit { - const edits = edit.edits.map(e => this.getSingleTextEdit(e)); + getTextEdit(edit: StringEdit): TextEdit { + const edits = edit.replacements.map(e => this.getSingleTextEdit(e)); return new TextEdit(edits); } } diff --git a/src/vs/editor/common/core/textLength.ts b/src/vs/editor/common/core/text/textLength.ts similarity index 96% rename from src/vs/editor/common/core/textLength.ts rename to src/vs/editor/common/core/text/textLength.ts index b063b099f83..8a404b7da8f 100644 --- a/src/vs/editor/common/core/textLength.ts +++ b/src/vs/editor/common/core/text/textLength.ts @@ -2,9 +2,9 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { LineRange } from './ranges/lineRange.js'; -import { Position } from './position.js'; -import { Range } from './range.js'; +import { LineRange } from '../ranges/lineRange.js'; +import { Position } from '../position.js'; +import { Range } from '../range.js'; /** * Represents a non-negative length of text in terms of line and column count. diff --git a/src/vs/editor/common/diff/defaultLinesDiffComputer/algorithms/diffAlgorithm.ts b/src/vs/editor/common/diff/defaultLinesDiffComputer/algorithms/diffAlgorithm.ts index 0d0eae6d7e1..ca97cdf39ad 100644 --- a/src/vs/editor/common/diff/defaultLinesDiffComputer/algorithms/diffAlgorithm.ts +++ b/src/vs/editor/common/diff/defaultLinesDiffComputer/algorithms/diffAlgorithm.ts @@ -5,7 +5,7 @@ import { forEachAdjacent } from '../../../../../base/common/arrays.js'; import { BugIndicatingError } from '../../../../../base/common/errors.js'; -import { OffsetRange } from '../../../core/offsetRange.js'; +import { OffsetRange } from '../../../core/ranges/offsetRange.js'; /** * Represents a synchronous diff algorithm. Should be executed in a worker. diff --git a/src/vs/editor/common/diff/defaultLinesDiffComputer/algorithms/dynamicProgrammingDiffing.ts b/src/vs/editor/common/diff/defaultLinesDiffComputer/algorithms/dynamicProgrammingDiffing.ts index 565560c5cf4..ff9306210b9 100644 --- a/src/vs/editor/common/diff/defaultLinesDiffComputer/algorithms/dynamicProgrammingDiffing.ts +++ b/src/vs/editor/common/diff/defaultLinesDiffComputer/algorithms/dynamicProgrammingDiffing.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { OffsetRange } from '../../../core/offsetRange.js'; +import { OffsetRange } from '../../../core/ranges/offsetRange.js'; import { IDiffAlgorithm, SequenceDiff, ISequence, ITimeout, InfiniteTimeout, DiffAlgorithmResult } from './diffAlgorithm.js'; import { Array2D } from '../utils.js'; diff --git a/src/vs/editor/common/diff/defaultLinesDiffComputer/algorithms/myersDiffAlgorithm.ts b/src/vs/editor/common/diff/defaultLinesDiffComputer/algorithms/myersDiffAlgorithm.ts index b496d002de8..9cdbdb8549f 100644 --- a/src/vs/editor/common/diff/defaultLinesDiffComputer/algorithms/myersDiffAlgorithm.ts +++ b/src/vs/editor/common/diff/defaultLinesDiffComputer/algorithms/myersDiffAlgorithm.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { OffsetRange } from '../../../core/offsetRange.js'; +import { OffsetRange } from '../../../core/ranges/offsetRange.js'; import { DiffAlgorithmResult, IDiffAlgorithm, ISequence, ITimeout, InfiniteTimeout, SequenceDiff } from './diffAlgorithm.js'; /** diff --git a/src/vs/editor/common/diff/defaultLinesDiffComputer/defaultLinesDiffComputer.ts b/src/vs/editor/common/diff/defaultLinesDiffComputer/defaultLinesDiffComputer.ts index 66f7b97442a..cd07eae5b0a 100644 --- a/src/vs/editor/common/diff/defaultLinesDiffComputer/defaultLinesDiffComputer.ts +++ b/src/vs/editor/common/diff/defaultLinesDiffComputer/defaultLinesDiffComputer.ts @@ -6,10 +6,10 @@ import { equals } from '../../../../base/common/arrays.js'; import { assertFn } from '../../../../base/common/assert.js'; import { LineRange } from '../../core/ranges/lineRange.js'; -import { OffsetRange } from '../../core/offsetRange.js'; +import { OffsetRange } from '../../core/ranges/offsetRange.js'; import { Position } from '../../core/position.js'; import { Range } from '../../core/range.js'; -import { ArrayText } from '../../core/edits/textEdit.js'; +import { ArrayText } from '../../core/text/abstractText.js'; import { ILinesDiffComputer, ILinesDiffComputerOptions, LinesDiff, MovedText } from '../linesDiffComputer.js'; import { DetailedLineRangeMapping, LineRangeMapping, lineRangeMappingFromRangeMappings, RangeMapping } from '../rangeMapping.js'; import { DateTimeout, InfiniteTimeout, ITimeout, SequenceDiff } from './algorithms/diffAlgorithm.js'; diff --git a/src/vs/editor/common/diff/defaultLinesDiffComputer/heuristicSequenceOptimizations.ts b/src/vs/editor/common/diff/defaultLinesDiffComputer/heuristicSequenceOptimizations.ts index 20de8dd1ab0..aaf411d7998 100644 --- a/src/vs/editor/common/diff/defaultLinesDiffComputer/heuristicSequenceOptimizations.ts +++ b/src/vs/editor/common/diff/defaultLinesDiffComputer/heuristicSequenceOptimizations.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { forEachWithNeighbors } from '../../../../base/common/arrays.js'; -import { OffsetRange } from '../../core/offsetRange.js'; +import { OffsetRange } from '../../core/ranges/offsetRange.js'; import { ISequence, OffsetPair, SequenceDiff } from './algorithms/diffAlgorithm.js'; import { LineSequence } from './lineSequence.js'; import { LinesSliceCharSequence } from './linesSliceCharSequence.js'; diff --git a/src/vs/editor/common/diff/defaultLinesDiffComputer/lineSequence.ts b/src/vs/editor/common/diff/defaultLinesDiffComputer/lineSequence.ts index 998459b21e3..019c72d01ef 100644 --- a/src/vs/editor/common/diff/defaultLinesDiffComputer/lineSequence.ts +++ b/src/vs/editor/common/diff/defaultLinesDiffComputer/lineSequence.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { CharCode } from '../../../../base/common/charCode.js'; -import { OffsetRange } from '../../core/offsetRange.js'; +import { OffsetRange } from '../../core/ranges/offsetRange.js'; import { ISequence } from './algorithms/diffAlgorithm.js'; export class LineSequence implements ISequence { diff --git a/src/vs/editor/common/diff/defaultLinesDiffComputer/linesSliceCharSequence.ts b/src/vs/editor/common/diff/defaultLinesDiffComputer/linesSliceCharSequence.ts index 25b4a6127f8..319a3298f9a 100644 --- a/src/vs/editor/common/diff/defaultLinesDiffComputer/linesSliceCharSequence.ts +++ b/src/vs/editor/common/diff/defaultLinesDiffComputer/linesSliceCharSequence.ts @@ -5,7 +5,7 @@ import { findLastIdxMonotonous, findLastMonotonous, findFirstMonotonous } from '../../../../base/common/arraysFind.js'; import { CharCode } from '../../../../base/common/charCode.js'; -import { OffsetRange } from '../../core/offsetRange.js'; +import { OffsetRange } from '../../core/ranges/offsetRange.js'; import { Position } from '../../core/position.js'; import { Range } from '../../core/range.js'; import { ISequence } from './algorithms/diffAlgorithm.js'; diff --git a/src/vs/editor/common/diff/rangeMapping.ts b/src/vs/editor/common/diff/rangeMapping.ts index 4dfbd9a36d7..c5a66c6a8d9 100644 --- a/src/vs/editor/common/diff/rangeMapping.ts +++ b/src/vs/editor/common/diff/rangeMapping.ts @@ -9,7 +9,8 @@ import { BugIndicatingError } from '../../../base/common/errors.js'; import { LineRange } from '../core/ranges/lineRange.js'; import { Position } from '../core/position.js'; import { Range } from '../core/range.js'; -import { AbstractText, SingleTextEdit, TextEdit } from '../core/edits/textEdit.js'; +import { TextReplacement, TextEdit } from '../core/edits/textEdit.js'; +import { AbstractText } from '../core/text/abstractText.js'; import { IChange } from './legacyLinesDiffComputer.js'; /** @@ -231,13 +232,13 @@ export class DetailedLineRangeMapping extends LineRangeMapping { export class RangeMapping { public static fromEdit(edit: TextEdit): RangeMapping[] { const newRanges = edit.getNewRanges(); - const result = edit.edits.map((e, idx) => new RangeMapping(e.range, newRanges[idx])); + const result = edit.replacements.map((e, idx) => new RangeMapping(e.range, newRanges[idx])); return result; } public static fromEditJoin(edit: TextEdit): RangeMapping { const newRanges = edit.getNewRanges(); - const result = edit.edits.map((e, idx) => new RangeMapping(e.range, newRanges[idx])); + const result = edit.replacements.map((e, idx) => new RangeMapping(e.range, newRanges[idx])); return RangeMapping.join(result); } @@ -294,9 +295,9 @@ export class RangeMapping { /** * Creates a single text edit that describes the change from the original to the modified text. */ - public toTextEdit(modified: AbstractText): SingleTextEdit { + public toTextEdit(modified: AbstractText): TextReplacement { const newText = modified.getValueOfRange(this.modifiedRange); - return new SingleTextEdit(this.originalRange, newText); + return new TextReplacement(this.originalRange, newText); } public join(other: RangeMapping): RangeMapping { diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index 5134e5e4d93..6046d803540 100644 --- a/src/vs/editor/common/model.ts +++ b/src/vs/editor/common/model.ts @@ -219,7 +219,7 @@ export interface IModelDecorationOptions { */ glyphMargin?: IModelDecorationGlyphMarginOptions | null; /** - * If set, the decoration will override the line height of the lines it spans. + * If set, the decoration will override the line height of the lines it spans. Maximum value is 300px. */ lineHeight?: number | null; /** diff --git a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/beforeEditPositionMapper.ts b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/beforeEditPositionMapper.ts index d5560fdd2ba..4663102eb36 100644 --- a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/beforeEditPositionMapper.ts +++ b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/beforeEditPositionMapper.ts @@ -5,7 +5,7 @@ import { Range } from '../../../core/range.js'; import { Length, lengthAdd, lengthDiffNonNegative, lengthLessThanEqual, lengthOfString, lengthToObj, positionToLength, toLength } from './length.js'; -import { TextLength } from '../../../core/textLength.js'; +import { TextLength } from '../../../core/text/textLength.js'; import { IModelContentChange } from '../../../textModelEvents.js'; export class TextEditInfo { diff --git a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/bracketPairsTree.ts b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/bracketPairsTree.ts index 42a40e99797..5a5bf23050a 100644 --- a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/bracketPairsTree.ts +++ b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/bracketPairsTree.ts @@ -25,7 +25,7 @@ import { combineTextEditInfos } from './combineTextEditInfos.js'; import { ClosingBracketKind, OpeningBracketKind } from '../../../languages/supports/languageBracketsConfiguration.js'; export class BracketPairsTree extends Disposable { - private readonly didChangeEmitter = new Emitter(); + private readonly didChangeEmitter; /* There are two trees: @@ -39,22 +39,28 @@ export class BracketPairsTree extends Disposable { private initialAstWithoutTokens: AstNode | undefined; private astWithTokens: AstNode | undefined; - private readonly denseKeyProvider = new DenseKeyProvider(); - private readonly brackets = new LanguageAgnosticBracketTokens(this.denseKeyProvider, this.getLanguageConfiguration); + private readonly denseKeyProvider; + private readonly brackets; public didLanguageChange(languageId: string): boolean { return this.brackets.didLanguageChange(languageId); } - public readonly onDidChange = this.didChangeEmitter.event; - private queuedTextEditsForInitialAstWithoutTokens: TextEditInfo[] = []; - private queuedTextEdits: TextEditInfo[] = []; + public readonly onDidChange; + private queuedTextEditsForInitialAstWithoutTokens: TextEditInfo[]; + private queuedTextEdits: TextEditInfo[]; public constructor( private readonly textModel: TextModel, private readonly getLanguageConfiguration: (languageId: string) => ResolvedLanguageConfiguration ) { super(); + this.didChangeEmitter = new Emitter(); + this.denseKeyProvider = new DenseKeyProvider(); + this.brackets = new LanguageAgnosticBracketTokens(this.denseKeyProvider, this.getLanguageConfiguration); + this.onDidChange = this.didChangeEmitter.event; + this.queuedTextEditsForInitialAstWithoutTokens = []; + this.queuedTextEdits = []; if (!textModel.tokenization.hasTokens) { const brackets = this.brackets.getSingleLanguageBracketTokens(this.textModel.getLanguageId()); diff --git a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/length.ts b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/length.ts index b858a7d4d79..26416a62ccd 100644 --- a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/length.ts +++ b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/length.ts @@ -6,7 +6,7 @@ import { splitLines } from '../../../../../base/common/strings.js'; import { Position } from '../../../core/position.js'; import { Range } from '../../../core/range.js'; -import { TextLength } from '../../../core/textLength.js'; +import { TextLength } from '../../../core/text/textLength.js'; /** * The end must be greater than or equal to the start. diff --git a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/tokenizer.ts b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/tokenizer.ts index ae8cce61d56..a425dc9334b 100644 --- a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/tokenizer.ts +++ b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/tokenizer.ts @@ -64,17 +64,21 @@ export class TextBufferTokenizer implements Tokenizer { private readonly textBufferLineCount: number; private readonly textBufferLastLineLength: number; - private readonly reader = new NonPeekableTextBufferTokenizer(this.textModel, this.bracketTokens); + private readonly reader; constructor( private readonly textModel: ITokenizerSource, private readonly bracketTokens: LanguageAgnosticBracketTokens ) { + this.reader = new NonPeekableTextBufferTokenizer(this.textModel, this.bracketTokens); + this._offset = lengthZero; + this.didPeek = false; + this.peeked = null; this.textBufferLineCount = textModel.getLineCount(); this.textBufferLastLineLength = textModel.getLineLength(this.textBufferLineCount); } - private _offset: Length = lengthZero; + private _offset: Length; get offset() { return this._offset; @@ -95,8 +99,8 @@ export class TextBufferTokenizer implements Tokenizer { this.reader.setPosition(obj.lineCount, obj.columnCount); } - private didPeek = false; - private peeked: Token | null = null; + private didPeek; + private peeked: Token | null; read(): Token | null { let token: Token | null; diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index b702b86937e..50b25b6eecb 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -117,6 +117,7 @@ let MODEL_ID = 0; const LIMIT_FIND_COUNT = 999; const LONG_LINE_BOUNDARY = 10000; +const LINE_HEIGHT_CEILING = 300; class TextModelSnapshot implements model.ITextSnapshot { @@ -2387,7 +2388,7 @@ export class ModelDecorationOptions implements model.IModelDecorationOptions { this.glyphMarginHoverMessage = options.glyphMarginHoverMessage || null; this.lineNumberHoverMessage = options.lineNumberHoverMessage || null; this.isWholeLine = options.isWholeLine || false; - this.lineHeight = options.lineHeight ?? null; + this.lineHeight = options.lineHeight ? Math.min(options.lineHeight, LINE_HEIGHT_CEILING) : null; this.showIfCollapsed = options.showIfCollapsed || false; this.collapseOnReplaceEdit = options.collapseOnReplaceEdit || false; this.overviewRuler = options.overviewRuler ? new ModelDecorationOverviewRulerOptions(options.overviewRuler) : null; diff --git a/src/vs/editor/common/model/textModelOffsetEdit.ts b/src/vs/editor/common/model/textModelOffsetEdit.ts deleted file mode 100644 index c49ea7d6fae..00000000000 --- a/src/vs/editor/common/model/textModelOffsetEdit.ts +++ /dev/null @@ -1,56 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { EditOperation } from '../core/editOperation.js'; -import { Range } from '../core/range.js'; -import { OffsetEdit, SingleOffsetEdit } from '../core/edits/offsetEdit.js'; -import { OffsetRange } from '../core/offsetRange.js'; -import { DetailedLineRangeMapping } from '../diff/rangeMapping.js'; -import { ITextModel, IIdentifiedSingleEditOperation } from '../model.js'; -import { IModelContentChange } from '../textModelEvents.js'; - - -export abstract class OffsetEdits { - - private constructor() { - // static utils only! - } - - static asEditOperations(offsetEdit: OffsetEdit, doc: ITextModel): IIdentifiedSingleEditOperation[] { - const edits: IIdentifiedSingleEditOperation[] = []; - for (const singleEdit of offsetEdit.edits) { - const range = Range.fromPositions( - doc.getPositionAt(singleEdit.replaceRange.start), - doc.getPositionAt(singleEdit.replaceRange.start + singleEdit.replaceRange.length) - ); - edits.push(EditOperation.replace(range, singleEdit.newText)); - } - return edits; - } - - static fromContentChanges(contentChanges: readonly IModelContentChange[]) { - const editsArr = contentChanges.map(c => new SingleOffsetEdit(OffsetRange.ofStartAndLength(c.rangeOffset, c.rangeLength), c.text)); - editsArr.reverse(); - const edits = new OffsetEdit(editsArr); - return edits; - } - - static fromLineRangeMapping(original: ITextModel, modified: ITextModel, changes: readonly DetailedLineRangeMapping[]): OffsetEdit { - const edits: SingleOffsetEdit[] = []; - for (const c of changes) { - for (const i of c.innerChanges ?? []) { - const newText = modified.getValueInRange(i.modifiedRange); - - const startOrig = original.getOffsetAt(i.originalRange.getStartPosition()); - const endExOrig = original.getOffsetAt(i.originalRange.getEndPosition()); - const origRange = new OffsetRange(startOrig, endExOrig); - - edits.push(new SingleOffsetEdit(origRange, newText)); - } - } - - return new OffsetEdit(edits); - } -} diff --git a/src/vs/editor/common/model/textModelStringEdit.ts b/src/vs/editor/common/model/textModelStringEdit.ts new file mode 100644 index 00000000000..81a3778fb9a --- /dev/null +++ b/src/vs/editor/common/model/textModelStringEdit.ts @@ -0,0 +1,61 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { EditOperation } from '../core/editOperation.js'; +import { Range } from '../core/range.js'; +import { StringEdit, StringReplacement } from '../core/edits/stringEdit.js'; +import { OffsetRange } from '../core/ranges/offsetRange.js'; +import { DetailedLineRangeMapping } from '../diff/rangeMapping.js'; +import { ITextModel, IIdentifiedSingleEditOperation } from '../model.js'; +import { IModelContentChange } from '../textModelEvents.js'; +import { LengthEdit } from '../core/edits/lengthEdit.js'; +import { countEOL } from '../core/misc/eolCounter.js'; + +export function offsetEditToEditOperations(offsetEdit: StringEdit, doc: ITextModel): IIdentifiedSingleEditOperation[] { + const edits: IIdentifiedSingleEditOperation[] = []; + for (const singleEdit of offsetEdit.replacements) { + const range = Range.fromPositions( + doc.getPositionAt(singleEdit.replaceRange.start), + doc.getPositionAt(singleEdit.replaceRange.start + singleEdit.replaceRange.length) + ); + edits.push(EditOperation.replace(range, singleEdit.newText)); + } + return edits; +} + +export function offsetEditFromContentChanges(contentChanges: readonly IModelContentChange[]) { + const editsArr = contentChanges.map(c => new StringReplacement(OffsetRange.ofStartAndLength(c.rangeOffset, c.rangeLength), c.text)); + editsArr.reverse(); + const edits = new StringEdit(editsArr); + return edits; +} + +export function offsetEditFromLineRangeMapping(original: ITextModel, modified: ITextModel, changes: readonly DetailedLineRangeMapping[]): StringEdit { + const edits: StringReplacement[] = []; + for (const c of changes) { + for (const i of c.innerChanges ?? []) { + const newText = modified.getValueInRange(i.modifiedRange); + + const startOrig = original.getOffsetAt(i.originalRange.getStartPosition()); + const endExOrig = original.getOffsetAt(i.originalRange.getEndPosition()); + const origRange = new OffsetRange(startOrig, endExOrig); + + edits.push(new StringReplacement(origRange, newText)); + } + } + + return new StringEdit(edits); +} + +export function linesLengthEditFromModelContentChange(c: IModelContentChange[]): LengthEdit { + const contentChanges = c.slice().reverse(); + const lengthEdits = contentChanges.map(c => LengthEdit.replace( + // Expand the edit range to include the entire line + new OffsetRange(c.range.startLineNumber - 1, c.range.endLineNumber), + countEOL(c.text)[0] + 1) + ); + const lengthEdit = LengthEdit.compose(lengthEdits); + return lengthEdit; +} diff --git a/src/vs/editor/common/model/textModelText.ts b/src/vs/editor/common/model/textModelText.ts index a580ae3c1e0..ef75a848fcb 100644 --- a/src/vs/editor/common/model/textModelText.ts +++ b/src/vs/editor/common/model/textModelText.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { Range } from '../core/range.js'; -import { AbstractText } from '../core/edits/textEdit.js'; -import { TextLength } from '../core/textLength.js'; +import { AbstractText } from '../core/text/abstractText.js'; +import { TextLength } from '../core/text/textLength.js'; import { ITextModel } from '../model.js'; export class TextModelText extends AbstractText { diff --git a/src/vs/editor/common/model/textModelTokens.ts b/src/vs/editor/common/model/textModelTokens.ts index d1b6fee80fe..1714d33cbf1 100644 --- a/src/vs/editor/common/model/textModelTokens.ts +++ b/src/vs/editor/common/model/textModelTokens.ts @@ -9,7 +9,7 @@ import { setTimeout0 } from '../../../base/common/platform.js'; import { StopWatch } from '../../../base/common/stopwatch.js'; import { countEOL } from '../core/misc/eolCounter.js'; import { LineRange } from '../core/ranges/lineRange.js'; -import { OffsetRange } from '../core/offsetRange.js'; +import { OffsetRange } from '../core/ranges/offsetRange.js'; import { Position } from '../core/position.js'; import { StandardTokenType } from '../encodedTokenAttributes.js'; import { EncodedTokenizationResult, IBackgroundTokenizationStore, IBackgroundTokenizer, ILanguageIdCodec, IState, ITokenizationSupport } from '../languages.js'; @@ -25,7 +25,7 @@ const enum Constants { } export class TokenizerWithStateStore { - private readonly initialState = this.tokenizationSupport.getInitialState() as TState; + private readonly initialState; public readonly store: TrackingTokenizationStateStore; @@ -33,6 +33,7 @@ export class TokenizerWithStateStore { lineCount: number, public readonly tokenizationSupport: ITokenizationSupport ) { + this.initialState = this.tokenizationSupport.getInitialState() as TState; this.store = new TrackingTokenizationStateStore(lineCount); } diff --git a/src/vs/editor/common/model/tokenizationTextModelPart.ts b/src/vs/editor/common/model/tokenizationTextModelPart.ts index b1e8573652c..41eef46762b 100644 --- a/src/vs/editor/common/model/tokenizationTextModelPart.ts +++ b/src/vs/editor/common/model/tokenizationTextModelPart.ts @@ -34,19 +34,19 @@ import { SparseTokensStore } from '../tokens/sparseTokensStore.js'; import { IInstantiationService } from '../../../platform/instantiation/common/instantiation.js'; export class TokenizationTextModelPart extends TextModelPart implements ITokenizationTextModelPart { - private readonly _semanticTokens: SparseTokensStore = new SparseTokensStore(this._languageService.languageIdCodec); + private readonly _semanticTokens: SparseTokensStore; - private readonly _onDidChangeLanguage: Emitter = this._register(new Emitter()); - public readonly onDidChangeLanguage: Event = this._onDidChangeLanguage.event; + private readonly _onDidChangeLanguage: Emitter; + public readonly onDidChangeLanguage: Event; - private readonly _onDidChangeLanguageConfiguration: Emitter = this._register(new Emitter()); - public readonly onDidChangeLanguageConfiguration: Event = this._onDidChangeLanguageConfiguration.event; + private readonly _onDidChangeLanguageConfiguration: Emitter; + public readonly onDidChangeLanguageConfiguration: Event; - private readonly _onDidChangeTokens: Emitter = this._register(new Emitter()); - public readonly onDidChangeTokens: Event = this._onDidChangeTokens.event; + private readonly _onDidChangeTokens: Emitter; + public readonly onDidChangeTokens: Event; private _tokens!: AbstractTokens; - private readonly _tokensDisposables: DisposableStore = this._register(new DisposableStore()); + private readonly _tokensDisposables: DisposableStore; constructor( private readonly _textModel: TextModel, @@ -58,6 +58,14 @@ export class TokenizationTextModelPart extends TextModelPart implements ITokeniz @IInstantiationService private readonly _instantiationService: IInstantiationService ) { super(); + this._semanticTokens = new SparseTokensStore(this._languageService.languageIdCodec); + this._onDidChangeLanguage = this._register(new Emitter()); + this.onDidChangeLanguage = this._onDidChangeLanguage.event; + this._onDidChangeLanguageConfiguration = this._register(new Emitter()); + this.onDidChangeLanguageConfiguration = this._onDidChangeLanguageConfiguration.event; + this._onDidChangeTokens = this._register(new Emitter()); + this.onDidChangeTokens = this._onDidChangeTokens.event; + this._tokensDisposables = this._register(new DisposableStore()); // We just look at registry changes to determine whether to use tree sitter. // This means that removing a language from the setting will not cause a switch to textmate and will require a reload. diff --git a/src/vs/editor/common/services/treeSitter/textModelTreeSitter.ts b/src/vs/editor/common/services/treeSitter/textModelTreeSitter.ts index bf8744a3878..c311f41d377 100644 --- a/src/vs/editor/common/services/treeSitter/textModelTreeSitter.ts +++ b/src/vs/editor/common/services/treeSitter/textModelTreeSitter.ts @@ -15,7 +15,7 @@ import { Emitter, Event } from '../../../../base/common/event.js'; import { CancellationToken, cancelOnDispose } from '../../../../base/common/cancellation.js'; import { Range } from '../../core/range.js'; import { LimitedQueue } from '../../../../base/common/async.js'; -import { TextLength } from '../../core/textLength.js'; +import { TextLength } from '../../core/text/textLength.js'; import { TreeSitterLanguages } from './treeSitterLanguages.js'; import { AppResourcePath, FileAccess } from '../../../../base/common/network.js'; import { IFileService } from '../../../../platform/files/common/files.js'; @@ -41,6 +41,7 @@ export class TextModelTreeSitter extends Disposable implements ITextModelTreeSit private _query: Parser.Query | undefined; // TODO: @alexr00 use a better data structure for this private readonly _injectionTreeSitterTrees: DisposableMap = this._register(new DisposableMap()); + private readonly _injectionTreeSitterLanguages: Map> = new Map(); // parent language -> injected languages private _versionId: number = 0; get parseResult(): ITreeSitterParseResult | undefined { return this._rootTreeSitterTree; } @@ -123,16 +124,19 @@ export class TextModelTreeSitter extends Disposable implements ITextModelTreeSit }); } - private async _handleTreeUpdate(e: TreeParseUpdateEvent, parentTreeResult?: ITreeSitterParseResult, parentLanguage?: string) { + private async _handleTreeUpdate(e: TreeParseUpdateEvent, parentTree?: Parser.Tree) { if (e.ranges && (e.versionId >= this._versionId)) { this._versionId = e.versionId; - const tree = parentTreeResult ?? this._rootTreeSitterTree!; + const tree = (parentTree ?? this._rootTreeSitterTree!.tree)?.copy(); let injections: Map | undefined; - if (tree.tree) { - injections = await this._collectInjections(tree.tree); + if (tree) { + injections = await this._collectInjections(tree); // kick off check for injected languages if (injections) { - this._processInjections(injections, tree, parentLanguage ?? this.textModel.getLanguageId(), e.includedModelChanges); + this._processInjections(injections, tree, e.language, e.includedModelChanges).then(() => { + // Clean up tree copy + tree.delete(); + }); } } @@ -173,18 +177,18 @@ export class TextModelTreeSitter extends Disposable implements ITextModelTreeSit return this._query; } - private async _collectInjections(tree: Parser.Tree): Promise | undefined> { + private async _collectInjections(copyOfTree: Parser.Tree): Promise | undefined> { const query = await this._getQuery(); if (!query) { return; } - if (!tree?.rootNode) { + if (!copyOfTree?.rootNode) { // need to check the root node here as `walk` will throw if not defined. return; } - const cursor = tree.walk(); + const cursor = copyOfTree.walk(); const injections: Map = new Map(); let hasNext = true; @@ -193,6 +197,7 @@ export class TextModelTreeSitter extends Disposable implements ITextModelTreeSit // Yield periodically await new Promise(resolve => setTimeout0(resolve)); } + cursor.delete(); return this._mergeAdjacentRanges(injections); } @@ -274,23 +279,18 @@ export class TextModelTreeSitter extends Disposable implements ITextModelTreeSit private async _processInjections( injections: Map, - parentTree: ITreeSitterParseResult, + copyOfParentTree: Parser.Tree, parentLanguage: string, modelChanges: IModelContentChangedEvent[] | undefined ): Promise { - if (injections.size === 0) { - this._injectionTreeSitterTrees.clearAndDisposeAll(); - return; - } - - const unseenInjections: Set = new Set(this._injectionTreeSitterTrees.keys()); + const unseenInjections: Set = this._injectionTreeSitterLanguages.get(parentLanguage) ?? new Set(); for (const [languageId, ranges] of injections) { const language = await this._treeSitterLanguages.getLanguage(languageId); if (!language) { continue; } - const treeSitterTree = await this._getOrCreateInjectedTree(languageId, language, parentTree, parentLanguage); + const treeSitterTree = await this._getOrCreateInjectedTree(languageId, language, copyOfParentTree, parentLanguage); if (treeSitterTree) { unseenInjections.delete(languageId); this._onDidChangeContent(treeSitterTree, modelChanges, ranges); @@ -304,15 +304,17 @@ export class TextModelTreeSitter extends Disposable implements ITextModelTreeSit private async _getOrCreateInjectedTree( languageId: string, language: Parser.Language, - parentTree: ITreeSitterParseResult, + copyOfParentTree: Parser.Tree, parentLanguage: string ): Promise { let treeSitterTree = this._injectionTreeSitterTrees.get(languageId); if (!treeSitterTree) { const Parser = await this._treeSitterImporter.getParserClass(); treeSitterTree = new TreeSitterParseResult(new Parser(), languageId, language, this._logService, this._telemetryService); - this._parseSessionDisposables.add(treeSitterTree.onDidUpdate(e => this._handleTreeUpdate(e, parentTree, parentLanguage))); + this._parseSessionDisposables.add(treeSitterTree.onDidUpdate(e => this._handleTreeUpdate(e, copyOfParentTree))); this._injectionTreeSitterTrees.set(languageId, treeSitterTree); + const injectionLanguages = this._injectionTreeSitterLanguages.get(parentLanguage) ?? this._injectionTreeSitterLanguages.set(parentLanguage, new Set()).get(parentLanguage)!; + injectionLanguages.add(languageId); } return treeSitterTree; } @@ -380,7 +382,10 @@ export class TreeSitterParseResult implements IDisposable, ITreeSitterParseResul get tree() { return this._lastFullyParsed; } get isDisposed() { return this._isDisposed; } - private findChangedNodes(newTree: Parser.Tree, oldTree: Parser.Tree): Parser.Range[] { + private findChangedNodes(newTree: Parser.Tree, oldTree: Parser.Tree): Parser.Range[] | undefined { + if ((this.ranges && this.ranges.every(range => range.startPosition.row !== newTree.rootNode.startPosition.row)) || newTree.rootNode.startPosition.row !== 0) { + return []; + } const newCursor = newTree.walk(); const oldCursor = oldTree.walk(); @@ -426,6 +431,8 @@ export class TreeSitterParseResult implements IDisposable, ITreeSitterParseResul } } while (next); + newCursor.delete(); + oldCursor.delete(); return nodes; } @@ -468,41 +475,10 @@ export class TreeSitterParseResult implements IDisposable, ITreeSitterParseResul } } - let nodesInRange: Parser.Node[]; - // It's possible we end up with a really large range if the parent node is big - // Try to avoid this large range by finding several smaller nodes that together encompass the range of the changed node. - const foundNodeSize = cursor.endIndex - cursor.startIndex; - if (foundNodeSize > 5000) { - // Try to find 3 consecutive nodes that together encompass the changed node. - let child = cursor.gotoFirstChild(); - nodesInRange = []; - while (child) { - if (cursor.endIndex > node.startIndex) { - // Found the starting point of our nodes - nodesInRange.push(cursor.currentNode); - do { - child = cursor.gotoNextSibling(); - } while (child && (cursor.endIndex < node.endIndex)); - - nodesInRange.push(cursor.currentNode); - break; - } - child = cursor.gotoNextSibling(); - } - } else { - nodesInRange = [cursor.currentNode]; - } - - // Fill in gaps between nodes - // Reset the cursor to the first node in the range; - while (cursor.currentNode.id !== nodesInRange[0].id) { - cursor.gotoPreviousSibling(); - } - const previousNode = getClosestPreviousNodes(cursor, newTree); - const startPosition = previousNode ? previousNode.endPosition : nodesInRange[0].startPosition; - const startIndex = previousNode ? previousNode.endIndex : nodesInRange[0].startIndex; - const endPosition = nodesInRange[nodesInRange.length - 1].endPosition; - const endIndex = nodesInRange[nodesInRange.length - 1].endIndex; + const startPosition = cursor.currentNode.startPosition; + const endPosition = cursor.currentNode.endPosition; + const startIndex = cursor.currentNode.startIndex; + const endIndex = cursor.currentNode.endIndex; const newChange = { newRange: new Range(startPosition.row + 1, startPosition.column + 1, endPosition.row + 1, endPosition.column + 1), newRangeStartOffset: startIndex, newRangeEndOffset: endIndex }; if ((newRangeIndex < newRanges.length) && rangesIntersect(newRanges[newRangeIndex], { startIndex, endIndex, startPosition, endPosition })) { diff --git a/src/vs/editor/common/standalone/standaloneEnums.ts b/src/vs/editor/common/standalone/standaloneEnums.ts index f44bee76294..bd615eafcf7 100644 --- a/src/vs/editor/common/standalone/standaloneEnums.ts +++ b/src/vs/editor/common/standalone/standaloneEnums.ts @@ -177,159 +177,160 @@ export enum EditorOption { acceptSuggestionOnEnter = 1, accessibilitySupport = 2, accessibilityPageSize = 3, - ariaLabel = 4, - ariaRequired = 5, - autoClosingBrackets = 6, - autoClosingComments = 7, - screenReaderAnnounceInlineSuggestion = 8, - autoClosingDelete = 9, - autoClosingOvertype = 10, - autoClosingQuotes = 11, - autoIndent = 12, - automaticLayout = 13, - autoSurround = 14, - bracketPairColorization = 15, - guides = 16, - codeLens = 17, - codeLensFontFamily = 18, - codeLensFontSize = 19, - colorDecorators = 20, - colorDecoratorsLimit = 21, - columnSelection = 22, - comments = 23, - contextmenu = 24, - copyWithSyntaxHighlighting = 25, - cursorBlinking = 26, - cursorSmoothCaretAnimation = 27, - cursorStyle = 28, - cursorSurroundingLines = 29, - cursorSurroundingLinesStyle = 30, - cursorWidth = 31, - disableLayerHinting = 32, - disableMonospaceOptimizations = 33, - domReadOnly = 34, - dragAndDrop = 35, - dropIntoEditor = 36, - experimentalEditContextEnabled = 37, - emptySelectionClipboard = 38, - experimentalGpuAcceleration = 39, - experimentalWhitespaceRendering = 40, - extraEditorClassName = 41, - fastScrollSensitivity = 42, - find = 43, - fixedOverflowWidgets = 44, - folding = 45, - foldingStrategy = 46, - foldingHighlight = 47, - foldingImportsByDefault = 48, - foldingMaximumRegions = 49, - unfoldOnClickAfterEndOfLine = 50, - fontFamily = 51, - fontInfo = 52, - fontLigatures = 53, - fontSize = 54, - fontWeight = 55, - fontVariations = 56, - formatOnPaste = 57, - formatOnType = 58, - glyphMargin = 59, - gotoLocation = 60, - hideCursorInOverviewRuler = 61, - hover = 62, - inDiffEditor = 63, - inlineSuggest = 64, - letterSpacing = 65, - lightbulb = 66, - lineDecorationsWidth = 67, - lineHeight = 68, - lineNumbers = 69, - lineNumbersMinChars = 70, - linkedEditing = 71, - links = 72, - matchBrackets = 73, - minimap = 74, - mouseStyle = 75, - mouseWheelScrollSensitivity = 76, - mouseWheelZoom = 77, - multiCursorMergeOverlapping = 78, - multiCursorModifier = 79, - multiCursorPaste = 80, - multiCursorLimit = 81, - occurrencesHighlight = 82, - occurrencesHighlightDelay = 83, - overtypeCursorStyle = 84, - overtypeOnPaste = 85, - overviewRulerBorder = 86, - overviewRulerLanes = 87, - padding = 88, - pasteAs = 89, - parameterHints = 90, - peekWidgetDefaultFocus = 91, - placeholder = 92, - definitionLinkOpensInPeek = 93, - quickSuggestions = 94, - quickSuggestionsDelay = 95, - readOnly = 96, - readOnlyMessage = 97, - renameOnType = 98, - renderControlCharacters = 99, - renderFinalNewline = 100, - renderLineHighlight = 101, - renderLineHighlightOnlyWhenFocus = 102, - renderValidationDecorations = 103, - renderWhitespace = 104, - revealHorizontalRightPadding = 105, - roundedSelection = 106, - rulers = 107, - scrollbar = 108, - scrollBeyondLastColumn = 109, - scrollBeyondLastLine = 110, - scrollPredominantAxis = 111, - selectionClipboard = 112, - selectionHighlight = 113, - selectOnLineNumbers = 114, - showFoldingControls = 115, - showUnused = 116, - snippetSuggestions = 117, - smartSelect = 118, - smoothScrolling = 119, - stickyScroll = 120, - stickyTabStops = 121, - stopRenderingLineAfter = 122, - suggest = 123, - suggestFontSize = 124, - suggestLineHeight = 125, - suggestOnTriggerCharacters = 126, - suggestSelection = 127, - tabCompletion = 128, - tabIndex = 129, - unicodeHighlighting = 130, - unusualLineTerminators = 131, - useShadowDOM = 132, - useTabStops = 133, - wordBreak = 134, - wordSegmenterLocales = 135, - wordSeparators = 136, - wordWrap = 137, - wordWrapBreakAfterCharacters = 138, - wordWrapBreakBeforeCharacters = 139, - wordWrapColumn = 140, - wordWrapOverride1 = 141, - wordWrapOverride2 = 142, - wrappingIndent = 143, - wrappingStrategy = 144, - showDeprecated = 145, - inlayHints = 146, - effectiveCursorStyle = 147, - editorClassName = 148, - pixelRatio = 149, - tabFocusMode = 150, - layoutInfo = 151, - wrappingInfo = 152, - defaultColorDecorators = 153, - colorDecoratorsActivatedOn = 154, - inlineCompletionsAccessibilityVerbose = 155, - effectiveExperimentalEditContextEnabled = 156 + allowVariableLineHeights = 4, + ariaLabel = 5, + ariaRequired = 6, + autoClosingBrackets = 7, + autoClosingComments = 8, + screenReaderAnnounceInlineSuggestion = 9, + autoClosingDelete = 10, + autoClosingOvertype = 11, + autoClosingQuotes = 12, + autoIndent = 13, + automaticLayout = 14, + autoSurround = 15, + bracketPairColorization = 16, + guides = 17, + codeLens = 18, + codeLensFontFamily = 19, + codeLensFontSize = 20, + colorDecorators = 21, + colorDecoratorsLimit = 22, + columnSelection = 23, + comments = 24, + contextmenu = 25, + copyWithSyntaxHighlighting = 26, + cursorBlinking = 27, + cursorSmoothCaretAnimation = 28, + cursorStyle = 29, + cursorSurroundingLines = 30, + cursorSurroundingLinesStyle = 31, + cursorWidth = 32, + disableLayerHinting = 33, + disableMonospaceOptimizations = 34, + domReadOnly = 35, + dragAndDrop = 36, + dropIntoEditor = 37, + experimentalEditContextEnabled = 38, + emptySelectionClipboard = 39, + experimentalGpuAcceleration = 40, + experimentalWhitespaceRendering = 41, + extraEditorClassName = 42, + fastScrollSensitivity = 43, + find = 44, + fixedOverflowWidgets = 45, + folding = 46, + foldingStrategy = 47, + foldingHighlight = 48, + foldingImportsByDefault = 49, + foldingMaximumRegions = 50, + unfoldOnClickAfterEndOfLine = 51, + fontFamily = 52, + fontInfo = 53, + fontLigatures = 54, + fontSize = 55, + fontWeight = 56, + fontVariations = 57, + formatOnPaste = 58, + formatOnType = 59, + glyphMargin = 60, + gotoLocation = 61, + hideCursorInOverviewRuler = 62, + hover = 63, + inDiffEditor = 64, + inlineSuggest = 65, + letterSpacing = 66, + lightbulb = 67, + lineDecorationsWidth = 68, + lineHeight = 69, + lineNumbers = 70, + lineNumbersMinChars = 71, + linkedEditing = 72, + links = 73, + matchBrackets = 74, + minimap = 75, + mouseStyle = 76, + mouseWheelScrollSensitivity = 77, + mouseWheelZoom = 78, + multiCursorMergeOverlapping = 79, + multiCursorModifier = 80, + multiCursorPaste = 81, + multiCursorLimit = 82, + occurrencesHighlight = 83, + occurrencesHighlightDelay = 84, + overtypeCursorStyle = 85, + overtypeOnPaste = 86, + overviewRulerBorder = 87, + overviewRulerLanes = 88, + padding = 89, + pasteAs = 90, + parameterHints = 91, + peekWidgetDefaultFocus = 92, + placeholder = 93, + definitionLinkOpensInPeek = 94, + quickSuggestions = 95, + quickSuggestionsDelay = 96, + readOnly = 97, + readOnlyMessage = 98, + renameOnType = 99, + renderControlCharacters = 100, + renderFinalNewline = 101, + renderLineHighlight = 102, + renderLineHighlightOnlyWhenFocus = 103, + renderValidationDecorations = 104, + renderWhitespace = 105, + revealHorizontalRightPadding = 106, + roundedSelection = 107, + rulers = 108, + scrollbar = 109, + scrollBeyondLastColumn = 110, + scrollBeyondLastLine = 111, + scrollPredominantAxis = 112, + selectionClipboard = 113, + selectionHighlight = 114, + selectOnLineNumbers = 115, + showFoldingControls = 116, + showUnused = 117, + snippetSuggestions = 118, + smartSelect = 119, + smoothScrolling = 120, + stickyScroll = 121, + stickyTabStops = 122, + stopRenderingLineAfter = 123, + suggest = 124, + suggestFontSize = 125, + suggestLineHeight = 126, + suggestOnTriggerCharacters = 127, + suggestSelection = 128, + tabCompletion = 129, + tabIndex = 130, + unicodeHighlighting = 131, + unusualLineTerminators = 132, + useShadowDOM = 133, + useTabStops = 134, + wordBreak = 135, + wordSegmenterLocales = 136, + wordSeparators = 137, + wordWrap = 138, + wordWrapBreakAfterCharacters = 139, + wordWrapBreakBeforeCharacters = 140, + wordWrapColumn = 141, + wordWrapOverride1 = 142, + wordWrapOverride2 = 143, + wrappingIndent = 144, + wrappingStrategy = 145, + showDeprecated = 146, + inlayHints = 147, + effectiveCursorStyle = 148, + editorClassName = 149, + pixelRatio = 150, + tabFocusMode = 151, + layoutInfo = 152, + wrappingInfo = 153, + defaultColorDecorators = 154, + colorDecoratorsActivatedOn = 155, + inlineCompletionsAccessibilityVerbose = 156, + effectiveExperimentalEditContextEnabled = 157 } /** diff --git a/src/vs/editor/common/tokens/lineTokens.ts b/src/vs/editor/common/tokens/lineTokens.ts index 1ebab1927d7..5beed680735 100644 --- a/src/vs/editor/common/tokens/lineTokens.ts +++ b/src/vs/editor/common/tokens/lineTokens.ts @@ -7,7 +7,7 @@ import { ILanguageIdCodec } from '../languages.js'; import { FontStyle, ColorId, StandardTokenType, MetadataConsts, TokenMetadata, ITokenPresentation } from '../encodedTokenAttributes.js'; import { IPosition } from '../core/position.js'; import { ITextModel } from '../model.js'; -import { OffsetRange } from '../core/offsetRange.js'; +import { OffsetRange } from '../core/ranges/offsetRange.js'; import { TokenArray, TokenArrayBuilder } from './tokenArray.js'; import { onUnexpectedError } from '../../../base/common/errors.js'; diff --git a/src/vs/editor/common/tokens/tokenArray.ts b/src/vs/editor/common/tokens/tokenArray.ts index 555e5654dcc..93f19a8ec97 100644 --- a/src/vs/editor/common/tokens/tokenArray.ts +++ b/src/vs/editor/common/tokens/tokenArray.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { OffsetRange } from '../core/offsetRange.js'; +import { OffsetRange } from '../core/ranges/offsetRange.js'; import { ILanguageIdCodec } from '../languages.js'; import { LineTokens } from './lineTokens.js'; diff --git a/src/vs/editor/common/tokens/tokenWithTextArray.ts b/src/vs/editor/common/tokens/tokenWithTextArray.ts index 765d480a12b..ba0385f9554 100644 --- a/src/vs/editor/common/tokens/tokenWithTextArray.ts +++ b/src/vs/editor/common/tokens/tokenWithTextArray.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { OffsetRange } from '../core/offsetRange.js'; +import { OffsetRange } from '../core/ranges/offsetRange.js'; import { ILanguageIdCodec } from '../languages.js'; import { LineTokens } from './lineTokens.js'; diff --git a/src/vs/editor/common/viewLayout/lineHeights.ts b/src/vs/editor/common/viewLayout/lineHeights.ts index 37996f9f967..47e402e075b 100644 --- a/src/vs/editor/common/viewLayout/lineHeights.ts +++ b/src/vs/editor/common/viewLayout/lineHeights.ts @@ -219,8 +219,10 @@ export class LineHeightsManager { const totalHeightDeleted = deleteCount * this._defaultLineHeight; for (let i = endIndexOfDeletion; i < this._orderedCustomLines.length; i++) { const customLine = this._orderedCustomLines[i]; - customLine.lineNumber -= deleteCount; - customLine.prefixSum -= totalHeightDeleted; + if (customLine.lineNumber > toLineNumber) { + customLine.lineNumber -= deleteCount; + customLine.prefixSum -= totalHeightDeleted; + } } } } diff --git a/src/vs/editor/common/viewLayout/viewLayout.ts b/src/vs/editor/common/viewLayout/viewLayout.ts index 404a5823b04..2f3091955d0 100644 --- a/src/vs/editor/common/viewLayout/viewLayout.ts +++ b/src/vs/editor/common/viewLayout/viewLayout.ts @@ -405,7 +405,7 @@ export class ViewLayout extends Disposable implements IViewLayout { public isInTopPadding(verticalOffset: number): boolean { return this._linesLayout.isInTopPadding(verticalOffset); } - isInBottomPadding(verticalOffset: number): boolean { + public isInBottomPadding(verticalOffset: number): boolean { return this._linesLayout.isInBottomPadding(verticalOffset); } diff --git a/src/vs/editor/common/viewLayout/viewLineRenderer.ts b/src/vs/editor/common/viewLayout/viewLineRenderer.ts index 199e3eae4d5..5911aa3dad1 100644 --- a/src/vs/editor/common/viewLayout/viewLineRenderer.ts +++ b/src/vs/editor/common/viewLayout/viewLineRenderer.ts @@ -11,7 +11,7 @@ import { StringBuilder } from '../core/stringBuilder.js'; import { LineDecoration, LineDecorationsNormalizer } from './lineDecorations.js'; import { InlineDecorationType } from '../viewModel.js'; import { LinePart, LinePartMetadata } from './linePart.js'; -import { OffsetRange } from '../core/offsetRange.js'; +import { OffsetRange } from '../core/ranges/offsetRange.js'; export const enum RenderWhitespace { None = 0, diff --git a/src/vs/editor/common/viewModel/viewModelImpl.ts b/src/vs/editor/common/viewModel/viewModelImpl.ts index 09d842ecd35..45d600a8dbd 100644 --- a/src/vs/editor/common/viewModel/viewModelImpl.ts +++ b/src/vs/editor/common/viewModel/viewModelImpl.ts @@ -185,6 +185,10 @@ export class ViewModel extends Disposable implements IViewModel { } private _getCustomLineHeights(): ICustomLineHeightData[] { + const allowVariableLineHeights = this._configuration.options.get(EditorOption.allowVariableLineHeights); + if (!allowVariableLineHeights) { + return []; + } const decorations = this.model.getCustomLineHeightsDecorations(this._editorId); return decorations.map((d) => { const lineNumber = d.range.startLineNumber; @@ -434,27 +438,30 @@ export class ViewModel extends Disposable implements IViewModel { this._handleVisibleLinesChanged(); })); - this._register(this.model.onDidChangeLineHeight((e) => { - const filteredChanges = e.changes.filter((change) => change.ownerId === this._editorId || change.ownerId === 0); + const allowVariableLineHeights = this._configuration.options.get(EditorOption.allowVariableLineHeights); + if (allowVariableLineHeights) { + this._register(this.model.onDidChangeLineHeight((e) => { + const filteredChanges = e.changes.filter((change) => change.ownerId === this._editorId || change.ownerId === 0); - this.viewLayout.changeSpecialLineHeights((accessor: ILineHeightChangeAccessor) => { - for (const change of filteredChanges) { - const { decorationId, lineNumber, lineHeight } = change; - const viewRange = this.coordinatesConverter.convertModelRangeToViewRange(new Range(lineNumber, 1, lineNumber, this.model.getLineMaxColumn(lineNumber))); - if (lineHeight !== null) { - accessor.insertOrChangeCustomLineHeight(decorationId, viewRange.startLineNumber, viewRange.endLineNumber, lineHeight); - } else { - accessor.removeCustomLineHeight(decorationId); + this.viewLayout.changeSpecialLineHeights((accessor: ILineHeightChangeAccessor) => { + for (const change of filteredChanges) { + const { decorationId, lineNumber, lineHeight } = change; + const viewRange = this.coordinatesConverter.convertModelRangeToViewRange(new Range(lineNumber, 1, lineNumber, this.model.getLineMaxColumn(lineNumber))); + if (lineHeight !== null) { + accessor.insertOrChangeCustomLineHeight(decorationId, viewRange.startLineNumber, viewRange.endLineNumber, lineHeight); + } else { + accessor.removeCustomLineHeight(decorationId); + } } - } - }); + }); - // recreate the model event using the filtered changes - if (filteredChanges.length > 0) { - const filteredEvent = new textModelEvents.ModelLineHeightChangedEvent(filteredChanges); - this._eventDispatcher.emitOutgoingEvent(new ModelLineHeightChangedEvent(filteredEvent)); - } - })); + // recreate the model event using the filtered changes + if (filteredChanges.length > 0) { + const filteredEvent = new textModelEvents.ModelLineHeightChangedEvent(filteredChanges); + this._eventDispatcher.emitOutgoingEvent(new ModelLineHeightChangedEvent(filteredEvent)); + } + })); + } this._register(this.model.onDidChangeTokens((e) => { const viewRanges: { fromLineNumber: number; toLineNumber: number }[] = []; diff --git a/src/vs/editor/contrib/hover/browser/contentHoverRendered.ts b/src/vs/editor/contrib/hover/browser/contentHoverRendered.ts index a80bdb7966f..25e5747ab6f 100644 --- a/src/vs/editor/contrib/hover/browser/contentHoverRendered.ts +++ b/src/vs/editor/contrib/hover/browser/contentHoverRendered.ts @@ -22,7 +22,7 @@ import { InlayHintsHover } from '../../inlayHints/browser/inlayHintsHover.js'; import { BugIndicatingError } from '../../../../base/common/errors.js'; import { HoverAction } from '../../../../base/browser/ui/hover/hoverWidget.js'; import { IHoverService } from '../../../../platform/hover/browser/hover.js'; -import { IOffsetRange } from '../../../common/core/offsetRange.js'; +import { IOffsetRange } from '../../../common/core/ranges/offsetRange.js'; export class RenderedContentHover extends Disposable { diff --git a/src/vs/editor/contrib/inlayHints/browser/inlayHintsController.ts b/src/vs/editor/contrib/inlayHints/browser/inlayHintsController.ts index 913e8075b3c..0bcc5b3fe67 100644 --- a/src/vs/editor/contrib/inlayHints/browser/inlayHintsController.ts +++ b/src/vs/editor/contrib/inlayHints/browser/inlayHintsController.ts @@ -109,9 +109,9 @@ export class InlayHintsController implements IEditorContribution { private readonly _disposables = new DisposableStore(); private readonly _sessionDisposables = new DisposableStore(); - private readonly _debounceInfo: IFeatureDebounceInformation; private readonly _decorationsMetadata = new Map(); - private readonly _ruleFactory = new DynamicCssRules(this._editor); + private readonly _debounceInfo: IFeatureDebounceInformation; + private readonly _ruleFactory: DynamicCssRules; private _cursorInfo?: { position: Position; notEarlierThan: number }; private _activeRenderMode = RenderMode.Normal; @@ -126,6 +126,7 @@ export class InlayHintsController implements IEditorContribution { @INotificationService private readonly _notificationService: INotificationService, @IInstantiationService private readonly _instaService: IInstantiationService, ) { + this._ruleFactory = new DynamicCssRules(this._editor); this._debounceInfo = _featureDebounce.for(_languageFeaturesService.inlayHintsProvider, 'InlayHint', { min: 25 }); this._disposables.add(_languageFeaturesService.inlayHintsProvider.onDidChange(() => this._update())); this._disposables.add(_editor.onDidChangeModel(() => this._update())); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts b/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts index e124b19e2b3..7aab79fce9c 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts @@ -59,69 +59,30 @@ export class InlineCompletionsController extends Disposable { return hotClassGetOriginalInstance(editor.getContribution(InlineCompletionsController.ID)); } - private readonly _editorObs = observableCodeEditor(this.editor); - private readonly _positions = derived(this, reader => this._editorObs.selections.read(reader)?.map(s => s.getEndPosition()) ?? [new Position(1, 1)]); + private readonly _editorObs; + private readonly _positions; - private readonly _suggestWidgetAdapter = this._register(new ObservableSuggestWidgetAdapter( - this._editorObs, - item => this.model.get()?.handleSuggestAccepted(item), - () => this.model.get()?.selectedInlineCompletion.get()?.getSingleTextEdit(), - )); + private readonly _suggestWidgetAdapter; - private readonly _enabledInConfig = observableFromEvent(this, this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineSuggest).enabled); - private readonly _isScreenReaderEnabled = observableFromEvent(this, this._accessibilityService.onDidChangeScreenReaderOptimized, () => this._accessibilityService.isScreenReaderOptimized()); - private readonly _editorDictationInProgress = observableFromEvent(this, - this._contextKeyService.onDidChangeContext, - () => this._contextKeyService.getContext(this.editor.getDomNode()).getValue('editorDictation.inProgress') === true - ); - private readonly _enabled = derived(this, reader => this._enabledInConfig.read(reader) && (!this._isScreenReaderEnabled.read(reader) || !this._editorDictationInProgress.read(reader))); + private readonly _enabledInConfig; + private readonly _isScreenReaderEnabled; + private readonly _editorDictationInProgress; + private readonly _enabled; - private readonly _debounceValue = this._debounceService.for( - this._languageFeaturesService.inlineCompletionsProvider, - 'InlineCompletionsDebounce', - { min: 50, max: 50 } - ); + private readonly _debounceValue; - private readonly _focusIsInMenu = observableValue(this, false); - private readonly _focusIsInEditorOrMenu = derived(this, reader => { - const editorHasFocus = this._editorObs.isFocused.read(reader); - const menuHasFocus = this._focusIsInMenu.read(reader); - return editorHasFocus || menuHasFocus; - }); + private readonly _focusIsInMenu; + private readonly _focusIsInEditorOrMenu; - private readonly _cursorIsInIndentation = derived(this, reader => { - const cursorPos = this._editorObs.cursorPosition.read(reader); - if (cursorPos === null) { return false; } - const model = this._editorObs.model.read(reader); - if (!model) { return false; } - this._editorObs.versionId.read(reader); - const indentMaxColumn = model.getLineIndentColumn(cursorPos.lineNumber); - return cursorPos.column <= indentMaxColumn; - }); + private readonly _cursorIsInIndentation; - public readonly model = derivedDisposable(this, reader => { - if (this._editorObs.isReadonly.read(reader)) { return undefined; } - const textModel = this._editorObs.model.read(reader); - if (!textModel) { return undefined; } + public readonly model; - const model: InlineCompletionsModel = this._instantiationService.createInstance( - InlineCompletionsModel, - textModel, - this._suggestWidgetAdapter.selectedItem, - this._editorObs.versionId, - this._positions, - this._debounceValue, - this._enabled, - this.editor, - ); - return model; - }).recomputeInitiallyAndOnChange(this._store); + private readonly _playAccessibilitySignal; - private readonly _playAccessibilitySignal = observableSignal(this); + private readonly _hideInlineEditOnSelectionChange; - private readonly _hideInlineEditOnSelectionChange = this._editorObs.getOption(EditorOption.inlineSuggest).map(val => true); - - protected readonly _view = this._register(this._instantiationService.createInstance(InlineCompletionsView, this.editor, this.model, this._focusIsInMenu)); + protected readonly _view; constructor( public readonly editor: ICodeEditor, @@ -136,6 +97,60 @@ export class InlineCompletionsController extends Disposable { @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, ) { super(); + this._editorObs = observableCodeEditor(this.editor); + this._positions = derived(this, reader => this._editorObs.selections.read(reader)?.map(s => s.getEndPosition()) ?? [new Position(1, 1)]); + this._suggestWidgetAdapter = this._register(new ObservableSuggestWidgetAdapter( + this._editorObs, + item => this.model.get()?.handleSuggestAccepted(item), + () => this.model.get()?.selectedInlineCompletion.get()?.getSingleTextEdit(), + )); + this._enabledInConfig = observableFromEvent(this, this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineSuggest).enabled); + this._isScreenReaderEnabled = observableFromEvent(this, this._accessibilityService.onDidChangeScreenReaderOptimized, () => this._accessibilityService.isScreenReaderOptimized()); + this._editorDictationInProgress = observableFromEvent(this, + this._contextKeyService.onDidChangeContext, + () => this._contextKeyService.getContext(this.editor.getDomNode()).getValue('editorDictation.inProgress') === true + ); + this._enabled = derived(this, reader => this._enabledInConfig.read(reader) && (!this._isScreenReaderEnabled.read(reader) || !this._editorDictationInProgress.read(reader))); + this._debounceValue = this._debounceService.for( + this._languageFeaturesService.inlineCompletionsProvider, + 'InlineCompletionsDebounce', + { min: 50, max: 50 } + ); + this._focusIsInMenu = observableValue(this, false); + this._focusIsInEditorOrMenu = derived(this, reader => { + const editorHasFocus = this._editorObs.isFocused.read(reader); + const menuHasFocus = this._focusIsInMenu.read(reader); + return editorHasFocus || menuHasFocus; + }); + this._cursorIsInIndentation = derived(this, reader => { + const cursorPos = this._editorObs.cursorPosition.read(reader); + if (cursorPos === null) { return false; } + const model = this._editorObs.model.read(reader); + if (!model) { return false; } + this._editorObs.versionId.read(reader); + const indentMaxColumn = model.getLineIndentColumn(cursorPos.lineNumber); + return cursorPos.column <= indentMaxColumn; + }); + this.model = derivedDisposable(this, reader => { + if (this._editorObs.isReadonly.read(reader)) { return undefined; } + const textModel = this._editorObs.model.read(reader); + if (!textModel) { return undefined; } + + const model: InlineCompletionsModel = this._instantiationService.createInstance( + InlineCompletionsModel, + textModel, + this._suggestWidgetAdapter.selectedItem, + this._editorObs.versionId, + this._positions, + this._debounceValue, + this._enabled, + this.editor, + ); + return model; + }).recomputeInitiallyAndOnChange(this._store); + this._playAccessibilitySignal = observableSignal(this); + this._hideInlineEditOnSelectionChange = this._editorObs.getOption(EditorOption.inlineSuggest).map(val => true); + this._view = this._register(this._instantiationService.createInstance(InlineCompletionsView, this.editor, this.model, this._focusIsInMenu)); InlineCompletionsController._instances.add(this); this._register(toDisposable(() => InlineCompletionsController._instances.delete(this))); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/hintsWidget/inlineCompletionsHintsWidget.ts b/src/vs/editor/contrib/inlineCompletions/browser/hintsWidget/inlineCompletionsHintsWidget.ts index d5b54d82af8..4ec0cb7895c 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/hintsWidget/inlineCompletionsHintsWidget.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/hintsWidget/inlineCompletionsHintsWidget.ts @@ -13,7 +13,7 @@ import { RunOnceScheduler } from '../../../../../base/common/async.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { createHotClass } from '../../../../../base/common/hotReloadHelpers.js'; import { Disposable, toDisposable } from '../../../../../base/common/lifecycle.js'; -import { IObservable, autorun, autorunWithStore, derived, derivedObservableWithCache, derivedWithStore, observableFromEvent } from '../../../../../base/common/observable.js'; +import { IObservable, autorun, autorunWithStore, derived, derivedObservableWithCache, observableFromEvent } from '../../../../../base/common/observable.js'; import { OS } from '../../../../../base/common/platform.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { localize } from '../../../../../nls.js'; @@ -37,27 +37,11 @@ import { InlineCompletionsModel } from '../model/inlineCompletionsModel.js'; import './inlineCompletionsHintsWidget.css'; export class InlineCompletionsHintsWidget extends Disposable { - private readonly alwaysShowToolbar = observableFromEvent(this, this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineSuggest).showToolbar === 'always'); + private readonly alwaysShowToolbar; - private sessionPosition: Position | undefined = undefined; + private sessionPosition: Position | undefined; - private readonly position = derived(this, reader => { - const ghostText = this.model.read(reader)?.primaryGhostText.read(reader); - - if (!this.alwaysShowToolbar.read(reader) || !ghostText || ghostText.parts.length === 0) { - this.sessionPosition = undefined; - return null; - } - - const firstColumn = ghostText.parts[0].column; - if (this.sessionPosition && this.sessionPosition.lineNumber !== ghostText.lineNumber) { - this.sessionPosition = undefined; - } - - const position = new Position(ghostText.lineNumber, Math.min(firstColumn, this.sessionPosition?.column ?? Number.MAX_SAFE_INTEGER)); - this.sessionPosition = position; - return position; - }); + private readonly position; constructor( private readonly editor: ICodeEditor, @@ -65,6 +49,25 @@ export class InlineCompletionsHintsWidget extends Disposable { @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); + this.alwaysShowToolbar = observableFromEvent(this, this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineSuggest).showToolbar === 'always'); + this.sessionPosition = undefined; + this.position = derived(this, reader => { + const ghostText = this.model.read(reader)?.primaryGhostText.read(reader); + + if (!this.alwaysShowToolbar.read(reader) || !ghostText || ghostText.parts.length === 0) { + this.sessionPosition = undefined; + return null; + } + + const firstColumn = ghostText.parts[0].column; + if (this.sessionPosition && this.sessionPosition.lineNumber !== ghostText.lineNumber) { + this.sessionPosition = undefined; + } + + const position = new Position(ghostText.lineNumber, Math.min(firstColumn, this.sessionPosition?.column ?? Number.MAX_SAFE_INTEGER)); + this.sessionPosition = position; + return position; + }); this._register(autorunWithStore((reader, store) => { /** @description setup content widget */ @@ -73,8 +76,8 @@ export class InlineCompletionsHintsWidget extends Disposable { return; } - const contentWidgetValue = derivedWithStore((reader, store) => { - const contentWidget = store.add(this.instantiationService.createInstance( + const contentWidgetValue = derived((reader) => { + const contentWidget = reader.store.add(this.instantiationService.createInstance( InlineSuggestionHintsContentWidget.hot.read(reader), this.editor, true, @@ -86,9 +89,9 @@ export class InlineCompletionsHintsWidget extends Disposable { () => { }, )); editor.addContentWidget(contentWidget); - store.add(toDisposable(() => editor.removeContentWidget(contentWidget))); + reader.store.add(toDisposable(() => editor.removeContentWidget(contentWidget))); - store.add(autorun(reader => { + reader.store.add(autorun(reader => { /** @description request explicit */ const position = this.position.read(reader); if (!position) { @@ -122,38 +125,15 @@ export class InlineSuggestionHintsContentWidget extends Disposable implements IC private static id = 0; - private readonly id = `InlineSuggestionHintsContentWidget${InlineSuggestionHintsContentWidget.id++}`; - public readonly allowEditorOverflow = true; - public readonly suppressMouseDown = false; + private readonly id; + public readonly allowEditorOverflow; + public readonly suppressMouseDown; - private readonly _warningMessageContentNode = derivedWithStore((reader, store) => { - const warning = this._warning.read(reader); - if (!warning) { - return undefined; - } - if (typeof warning.message === 'string') { - return warning.message; - } - const markdownElement = store.add(renderMarkdown(warning.message)); - return markdownElement.element; - }); + private readonly _warningMessageContentNode; - private readonly _warningMessageNode = n.div({ - class: 'warningMessage', - style: { - maxWidth: 400, - margin: 4, - marginBottom: 4, - display: derived(reader => this._warning.read(reader) ? 'block' : 'none'), - } - }, [ - this._warningMessageContentNode, - ]).keepUpdated(this._store); + private readonly _warningMessageNode; - private readonly nodes = h('div.inlineSuggestionsHints', { className: this.withBorder ? 'monaco-hover monaco-hover-content' : '' }, [ - this._warningMessageNode.element, - h('div@toolBar'), - ]); + private readonly nodes; private createCommandAction(commandId: string, label: string, iconClassName: string): Action { const action = new Action( @@ -172,25 +152,18 @@ export class InlineSuggestionHintsContentWidget extends Disposable implements IC return action; } - private readonly previousAction = this._register(this.createCommandAction(showPreviousInlineSuggestionActionId, localize('previous', 'Previous'), ThemeIcon.asClassName(inlineSuggestionHintsPreviousIcon))); - private readonly availableSuggestionCountAction = this._register(new Action('inlineSuggestionHints.availableSuggestionCount', '', undefined, false)); - private readonly nextAction = this._register(this.createCommandAction(showNextInlineSuggestionActionId, localize('next', 'Next'), ThemeIcon.asClassName(inlineSuggestionHintsNextIcon))); + private readonly previousAction; + private readonly availableSuggestionCountAction; + private readonly nextAction; private readonly toolBar: CustomizedMenuWorkbenchToolBar; // TODO@hediet: deprecate MenuId.InlineCompletionsActions - private readonly inlineCompletionsActionsMenus = this._register(this._menuService.createMenu( - MenuId.InlineCompletionsActions, - this._contextKeyService - )); + private readonly inlineCompletionsActionsMenus; - private readonly clearAvailableSuggestionCountLabelDebounced = this._register(new RunOnceScheduler(() => { - this.availableSuggestionCountAction.label = ''; - }, 100)); + private readonly clearAvailableSuggestionCountLabelDebounced; - private readonly disableButtonsDebounced = this._register(new RunOnceScheduler(() => { - this.previousAction.enabled = this.nextAction.enabled = false; - }, 100)); + private readonly disableButtonsDebounced; constructor( private readonly editor: ICodeEditor, @@ -208,6 +181,48 @@ export class InlineSuggestionHintsContentWidget extends Disposable implements IC @IMenuService private readonly _menuService: IMenuService, ) { super(); + this.id = `InlineSuggestionHintsContentWidget${InlineSuggestionHintsContentWidget.id++}`; + this.allowEditorOverflow = true; + this.suppressMouseDown = false; + this._warningMessageContentNode = derived((reader) => { + const warning = this._warning.read(reader); + if (!warning) { + return undefined; + } + if (typeof warning.message === 'string') { + return warning.message; + } + const markdownElement = reader.store.add(renderMarkdown(warning.message)); + return markdownElement.element; + }); + this._warningMessageNode = n.div({ + class: 'warningMessage', + style: { + maxWidth: 400, + margin: 4, + marginBottom: 4, + display: derived(reader => this._warning.read(reader) ? 'block' : 'none'), + } + }, [ + this._warningMessageContentNode, + ]).keepUpdated(this._store); + this.nodes = h('div.inlineSuggestionsHints', { className: this.withBorder ? 'monaco-hover monaco-hover-content' : '' }, [ + this._warningMessageNode.element, + h('div@toolBar'), + ]); + this.previousAction = this._register(this.createCommandAction(showPreviousInlineSuggestionActionId, localize('previous', 'Previous'), ThemeIcon.asClassName(inlineSuggestionHintsPreviousIcon))); + this.availableSuggestionCountAction = this._register(new Action('inlineSuggestionHints.availableSuggestionCount', '', undefined, false)); + this.nextAction = this._register(this.createCommandAction(showNextInlineSuggestionActionId, localize('next', 'Next'), ThemeIcon.asClassName(inlineSuggestionHintsNextIcon))); + this.inlineCompletionsActionsMenus = this._register(this._menuService.createMenu( + MenuId.InlineCompletionsActions, + this._contextKeyService + )); + this.clearAvailableSuggestionCountLabelDebounced = this._register(new RunOnceScheduler(() => { + this.availableSuggestionCountAction.label = ''; + }, 100)); + this.disableButtonsDebounced = this._register(new RunOnceScheduler(() => { + this.previousAction.enabled = this.nextAction.enabled = false; + }, 100)); this._register(autorun(reader => { this._warningMessageContentNode.read(reader); @@ -356,10 +371,10 @@ class StatusBarViewItem extends MenuEntryActionViewItem { } export class CustomizedMenuWorkbenchToolBar extends WorkbenchToolBar { - private readonly menu = this._store.add(this.menuService.createMenu(this.menuId, this.contextKeyService, { emitEventsForSubmenuChanges: true })); - private additionalActions: IAction[] = []; - private prependedPrimaryActions: IAction[] = []; - private additionalPrimaryActions: IAction[] = []; + private readonly menu; + private additionalActions: IAction[]; + private prependedPrimaryActions: IAction[]; + private additionalPrimaryActions: IAction[]; constructor( container: HTMLElement, @@ -373,6 +388,10 @@ export class CustomizedMenuWorkbenchToolBar extends WorkbenchToolBar { @ITelemetryService telemetryService: ITelemetryService, ) { super(container, { resetMenu: menuId, ...options2 }, menuService, contextKeyService, contextMenuService, keybindingService, commandService, telemetryService); + this.menu = this._store.add(this.menuService.createMenu(this.menuId, this.contextKeyService, { emitEventsForSubmenuChanges: true })); + this.additionalActions = []; + this.prependedPrimaryActions = []; + this.additionalPrimaryActions = []; this._store.add(this.menu.onDidChange(() => this.updateToolbar())); this.updateToolbar(); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/changeRecorder.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/changeRecorder.ts index 34f134540a6..1da5f48bf09 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/changeRecorder.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/changeRecorder.ts @@ -43,15 +43,16 @@ export class TextModelChangeRecorder extends Disposable { return result; } - private readonly _structuredLogger = this._register(this._instantiationService.createInstance(StructuredLogger.cast(), - 'editor.inlineSuggest.logChangeReason.commandId' - )); + private readonly _structuredLogger; constructor( private readonly _editor: ICodeEditor, @IInstantiationService private readonly _instantiationService: IInstantiationService, ) { super(); + this._structuredLogger = this._register(this._instantiationService.createInstance(StructuredLogger.cast(), + 'editor.inlineSuggest.logChangeReason.commandId' + )); this._register(autorunWithStore((reader, store) => { if (!(this._editor instanceof CodeEditorWidget)) { return; } if (!this._structuredLogger.isEnabled.read(reader)) { return; } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/computeGhostText.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/computeGhostText.ts index d71e46456f7..8a84aa75881 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/computeGhostText.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/computeGhostText.ts @@ -7,7 +7,7 @@ import { IDiffChange, LcsDiff } from '../../../../../base/common/diff/diff.js'; import { getLeadingWhitespace } from '../../../../../base/common/strings.js'; import { Position } from '../../../../common/core/position.js'; import { Range } from '../../../../common/core/range.js'; -import { SingleTextEdit } from '../../../../common/core/edits/textEdit.js'; +import { TextReplacement } from '../../../../common/core/edits/textEdit.js'; import { ITextModel } from '../../../../common/model.js'; import { GhostText, GhostTextPart } from './ghostText.js'; import { singleTextRemoveCommonPrefix } from './singleTextEditHelpers.js'; @@ -17,7 +17,7 @@ import { singleTextRemoveCommonPrefix } from './singleTextEditHelpers.js'; * If the text is `hello` and the suffix length is 2, the non-preview part is `hel` and the preview-part is `lo`. */ export function computeGhostText( - edit: SingleTextEdit, + edit: TextReplacement, model: ITextModel, mode: 'prefix' | 'subword' | 'subwordSmart', cursorPosition?: Position, @@ -58,7 +58,7 @@ export function computeGhostText( // Changes or removes existing indentation. Only add ghost text for the non-indentation part. : e.text.substring(suggestionAddedIndentationLength); - e = new SingleTextEdit(rangeThatDoesNotReplaceIndentation, suggestionWithoutIndentationChange); + e = new TextReplacement(rangeThatDoesNotReplaceIndentation, suggestionWithoutIndentationChange); } // This is a single line string diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/ghostText.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/ghostText.ts index 73dc371b7d9..699f23dd094 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/ghostText.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/ghostText.ts @@ -7,7 +7,7 @@ import { equals } from '../../../../../base/common/arrays.js'; import { splitLines } from '../../../../../base/common/strings.js'; import { Position } from '../../../../common/core/position.js'; import { Range } from '../../../../common/core/range.js'; -import { SingleTextEdit, TextEdit } from '../../../../common/core/edits/textEdit.js'; +import { TextReplacement, TextEdit } from '../../../../common/core/edits/textEdit.js'; import { LineDecoration } from '../../../../common/viewLayout/lineDecorations.js'; import { InlineDecoration } from '../../../../common/viewModel.js'; import { ColumnRange } from '../../../../common/core/ranges/columnRange.js'; @@ -30,7 +30,7 @@ export class GhostText { */ render(documentText: string, debug: boolean = false): string { return new TextEdit([ - ...this.parts.map(p => new SingleTextEdit( + ...this.parts.map(p => new TextReplacement( Range.fromPositions(new Position(this.lineNumber, p.column)), debug ? `[${p.lines.map(line => line.line).join('\n')}]` : p.lines.map(line => line.line).join('\n') )), @@ -45,7 +45,7 @@ export class GhostText { const cappedLineText = lineText.substr(0, lastPart.column - 1); const text = new TextEdit([ - ...this.parts.map(p => new SingleTextEdit( + ...this.parts.map(p => new TextReplacement( Range.fromPositions(new Position(1, p.column)), p.lines.map(line => line.line).join('\n') )), @@ -127,12 +127,12 @@ export class GhostTextReplacement { if (debug) { return new TextEdit([ - new SingleTextEdit(Range.fromPositions(replaceRange.getStartPosition()), '('), - new SingleTextEdit(Range.fromPositions(replaceRange.getEndPosition()), `)[${this.newLines.join('\n')}]`), + new TextReplacement(Range.fromPositions(replaceRange.getStartPosition()), '('), + new TextReplacement(Range.fromPositions(replaceRange.getEndPosition()), `)[${this.newLines.join('\n')}]`), ]).applyToString(documentText); } else { return new TextEdit([ - new SingleTextEdit(replaceRange, this.newLines.join('\n')), + new TextReplacement(replaceRange, this.newLines.join('\n')), ]).applyToString(documentText); } } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts index 1a4e67074f7..7e036e8f0ef 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts @@ -23,8 +23,8 @@ import { LineRange } from '../../../../common/core/ranges/lineRange.js'; import { Position } from '../../../../common/core/position.js'; import { Range } from '../../../../common/core/range.js'; import { Selection } from '../../../../common/core/selection.js'; -import { SingleTextEdit, TextEdit } from '../../../../common/core/edits/textEdit.js'; -import { TextLength } from '../../../../common/core/textLength.js'; +import { TextReplacement, TextEdit } from '../../../../common/core/edits/textEdit.js'; +import { TextLength } from '../../../../common/core/text/textLength.js'; import { ScrollType } from '../../../../common/editorCommon.js'; import { Command, InlineCompletionEndOfLifeReasonKind, InlineCompletion, InlineCompletionTriggerKind, PartialAcceptTriggerKind, InlineCompletionsProvider } from '../../../../common/languages.js'; import { ILanguageConfigurationService } from '../../../../common/languages/languageConfigurationRegistry.js'; @@ -47,31 +47,31 @@ import { singleTextEditAugments, singleTextRemoveCommonPrefix } from './singleTe import { SuggestItemInfo } from './suggestWidgetAdapter.js'; export class InlineCompletionsModel extends Disposable { - private readonly _source = this._register(this._instantiationService.createInstance(InlineCompletionsSource, this.textModel, this._textModelVersionId, this._debounceValue)); - private readonly _isActive = observableValue(this, false); - private readonly _onlyRequestInlineEditsSignal = observableSignal(this); - private readonly _forceUpdateExplicitlySignal = observableSignal(this); - private readonly _noDelaySignal = observableSignal(this); + private readonly _source; + private readonly _isActive; + private readonly _onlyRequestInlineEditsSignal; + private readonly _forceUpdateExplicitlySignal; + private readonly _noDelaySignal; - private readonly _fetchSpecificProviderSignal = observableSignal(this); + private readonly _fetchSpecificProviderSignal; // We use a semantic id to keep the same inline completion selected even if the provider reorders the completions. - private readonly _selectedInlineCompletionId = observableValue(this, undefined); - public readonly primaryPosition = derived(this, reader => this._positions.read(reader)[0] ?? new Position(1, 1)); + private readonly _selectedInlineCompletionId; + public readonly primaryPosition; - private _isAcceptingPartially = false; + private _isAcceptingPartially; public get isAcceptingPartially() { return this._isAcceptingPartially; } - private readonly _onDidAccept = new Emitter(); - public readonly onDidAccept = this._onDidAccept.event; + private readonly _onDidAccept; + public readonly onDidAccept; - private readonly _editorObs = observableCodeEditor(this._editor); + private readonly _editorObs; - private readonly _suggestPreviewEnabled = this._editorObs.getOption(EditorOption.suggest).map(v => v.preview); - private readonly _suggestPreviewMode = this._editorObs.getOption(EditorOption.suggest).map(v => v.previewMode); - private readonly _inlineSuggestMode = this._editorObs.getOption(EditorOption.inlineSuggest).map(v => v.mode); - private readonly _inlineEditsEnabled = this._editorObs.getOption(EditorOption.inlineSuggest).map(v => !!v.edits.enabled); - private readonly _inlineEditsShowCollapsedEnabled = this._editorObs.getOption(EditorOption.inlineSuggest).map(s => s.edits.showCollapsed); + private readonly _suggestPreviewEnabled; + private readonly _suggestPreviewMode; + private readonly _inlineSuggestMode; + private readonly _inlineEditsEnabled; + private readonly _inlineEditsShowCollapsedEnabled; constructor( public readonly textModel: ITextModel, @@ -88,6 +88,435 @@ export class InlineCompletionsModel extends Disposable { @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, ) { super(); + this._source = this._register(this._instantiationService.createInstance(InlineCompletionsSource, this.textModel, this._textModelVersionId, this._debounceValue)); + this._isActive = observableValue(this, false); + this._onlyRequestInlineEditsSignal = observableSignal(this); + this._forceUpdateExplicitlySignal = observableSignal(this); + this._noDelaySignal = observableSignal(this); + this._fetchSpecificProviderSignal = observableSignal(this); + this._selectedInlineCompletionId = observableValue(this, undefined); + this.primaryPosition = derived(this, reader => this._positions.read(reader)[0] ?? new Position(1, 1)); + this._isAcceptingPartially = false; + this._onDidAccept = new Emitter(); + this.onDidAccept = this._onDidAccept.event; + this._editorObs = observableCodeEditor(this._editor); + this._suggestPreviewEnabled = this._editorObs.getOption(EditorOption.suggest).map(v => v.preview); + this._suggestPreviewMode = this._editorObs.getOption(EditorOption.suggest).map(v => v.previewMode); + this._inlineSuggestMode = this._editorObs.getOption(EditorOption.inlineSuggest).map(v => v.mode); + this._inlineEditsEnabled = this._editorObs.getOption(EditorOption.inlineSuggest).map(v => !!v.edits.enabled); + this._inlineEditsShowCollapsedEnabled = this._editorObs.getOption(EditorOption.inlineSuggest).map(s => s.edits.showCollapsed); + this._lastShownInlineCompletionInfo = undefined; + this._lastAcceptedInlineCompletionInfo = undefined; + this._didUndoInlineEdits = derivedHandleChanges({ + owner: this, + changeTracker: { + createChangeSummary: () => ({ didUndo: false }), + handleChange: (ctx, changeSummary) => { + changeSummary.didUndo = ctx.didChange(this._textModelVersionId) && !!ctx.change?.isUndoing; + return true; + } + } + }, (reader, changeSummary) => { + const versionId = this._textModelVersionId.read(reader); + if (versionId !== null + && this._lastAcceptedInlineCompletionInfo + && this._lastAcceptedInlineCompletionInfo.textModelVersionIdAfter === versionId - 1 + && this._lastAcceptedInlineCompletionInfo.inlineCompletion.isInlineEdit + && changeSummary.didUndo + ) { + this._lastAcceptedInlineCompletionInfo = undefined; + return true; + } + return false; + }); + this._preserveCurrentCompletionReasons = new Set([ + VersionIdChangeReason.Redo, + VersionIdChangeReason.Undo, + VersionIdChangeReason.AcceptWord, + ]); + this.dontRefetchSignal = observableSignal(this); + this._fetchInlineCompletionsPromise = derivedHandleChanges({ + owner: this, + changeTracker: { + createChangeSummary: () => ({ + dontRefetch: false, + preserveCurrentCompletion: false, + inlineCompletionTriggerKind: InlineCompletionTriggerKind.Automatic, + onlyRequestInlineEdits: false, + shouldDebounce: true, + provider: undefined as InlineCompletionsProvider | undefined, + }), + handleChange: (ctx, changeSummary) => { + /** @description fetch inline completions */ + if (ctx.didChange(this._textModelVersionId) && this._preserveCurrentCompletionReasons.has(this._getReason(ctx.change))) { + changeSummary.preserveCurrentCompletion = true; + } else if (ctx.didChange(this._forceUpdateExplicitlySignal)) { + changeSummary.inlineCompletionTriggerKind = InlineCompletionTriggerKind.Explicit; + } else if (ctx.didChange(this.dontRefetchSignal)) { + changeSummary.dontRefetch = true; + } else if (ctx.didChange(this._onlyRequestInlineEditsSignal)) { + changeSummary.onlyRequestInlineEdits = true; + } else if (ctx.didChange(this._fetchSpecificProviderSignal)) { + changeSummary.provider = ctx.change; + } + return true; + }, + }, + }, (reader, changeSummary) => { + this._source.clearOperationOnTextModelChange.read(reader); // Make sure the clear operation runs before the fetch operation + this._noDelaySignal.read(reader); + this.dontRefetchSignal.read(reader); + this._onlyRequestInlineEditsSignal.read(reader); + this._forceUpdateExplicitlySignal.read(reader); + this._fetchSpecificProviderSignal.read(reader); + const shouldUpdate = (this._enabled.read(reader) && this._selectedSuggestItem.read(reader)) || this._isActive.read(reader); + if (!shouldUpdate) { + this._source.cancelUpdate(); + return undefined; + } + + this._textModelVersionId.read(reader); // Refetch on text change + + const suggestWidgetInlineCompletions = this._source.suggestWidgetInlineCompletions.get(); + const suggestItem = this._selectedSuggestItem.read(reader); + if (suggestWidgetInlineCompletions && !suggestItem) { + this._source.seedInlineCompletionsWithSuggestWidget(); + } + + const cursorPosition = this.primaryPosition.get(); + if (changeSummary.dontRefetch) { + return Promise.resolve(true); + } + + if (this._didUndoInlineEdits.read(reader) && changeSummary.inlineCompletionTriggerKind !== InlineCompletionTriggerKind.Explicit) { + transaction(tx => { + this._source.clear(tx); + }); + return undefined; + } + + let context: InlineCompletionContextWithoutUuid = { + triggerKind: changeSummary.inlineCompletionTriggerKind, + selectedSuggestionInfo: suggestItem?.toSelectedSuggestionInfo(), + includeInlineCompletions: !changeSummary.onlyRequestInlineEdits, + includeInlineEdits: this._inlineEditsEnabled.read(reader), + }; + + if (context.triggerKind === InlineCompletionTriggerKind.Automatic) { + if (this.textModel.getAlternativeVersionId() === this._lastShownInlineCompletionInfo?.alternateTextModelVersionId) { + // When undoing back to a version where an inline edit/completion was shown, + // we want to show an inline edit (or completion) again if it was originally an inline edit (or completion). + context = { + ...context, + includeInlineCompletions: !this._lastShownInlineCompletionInfo.inlineCompletion.isInlineEdit, + includeInlineEdits: this._lastShownInlineCompletionInfo.inlineCompletion.isInlineEdit, + }; + } + } + + const itemToPreserveCandidate = this.selectedInlineCompletion.get() ?? this._inlineCompletionItems.get()?.inlineEdit; + const itemToPreserve = changeSummary.preserveCurrentCompletion || itemToPreserveCandidate?.forwardStable + ? itemToPreserveCandidate : undefined; + const userJumpedToActiveCompletion = this._jumpedToId.map(jumpedTo => !!jumpedTo && jumpedTo === this._inlineCompletionItems.get()?.inlineEdit?.semanticId); + + const providers = changeSummary.provider ? [changeSummary.provider] : this._languageFeaturesService.inlineCompletionsProvider.all(this.textModel); + + return this._source.fetch(providers, cursorPosition, context, itemToPreserve?.identity, changeSummary.shouldDebounce, userJumpedToActiveCompletion, !!changeSummary.provider); + }); + this._inlineCompletionItems = derivedOpts({ owner: this }, reader => { + const c = this._source.inlineCompletions.read(reader); + if (!c) { return undefined; } + const cursorPosition = this.primaryPosition.read(reader); + let inlineEdit: InlineEditItem | undefined = undefined; + const visibleCompletions: InlineCompletionItem[] = []; + for (const completion of c.inlineCompletions) { + if (!completion.isInlineEdit) { + if (completion.isVisible(this.textModel, cursorPosition)) { + visibleCompletions.push(completion); + } + } else { + inlineEdit = completion; + } + } + + if (visibleCompletions.length !== 0) { + // Don't show the inline edit if there is a visible completion + inlineEdit = undefined; + } + + return { + inlineCompletions: visibleCompletions, + inlineEdit, + }; + }); + this._filteredInlineCompletionItems = derivedOpts({ owner: this, equalsFn: itemsEquals() }, reader => { + const c = this._inlineCompletionItems.read(reader); + return c?.inlineCompletions ?? []; + }); + this.selectedInlineCompletionIndex = derived(this, (reader) => { + const selectedInlineCompletionId = this._selectedInlineCompletionId.read(reader); + const filteredCompletions = this._filteredInlineCompletionItems.read(reader); + const idx = this._selectedInlineCompletionId === undefined ? -1 + : filteredCompletions.findIndex(v => v.semanticId === selectedInlineCompletionId); + if (idx === -1) { + // Reset the selection so that the selection does not jump back when it appears again + this._selectedInlineCompletionId.set(undefined, undefined); + return 0; + } + return idx; + }); + this.selectedInlineCompletion = derived(this, (reader) => { + const filteredCompletions = this._filteredInlineCompletionItems.read(reader); + const idx = this.selectedInlineCompletionIndex.read(reader); + return filteredCompletions[idx]; + }); + this.activeCommands = derivedOpts({ owner: this, equalsFn: itemsEquals() }, + r => this.selectedInlineCompletion.read(r)?.source.inlineSuggestions.commands ?? [] + ); + this.lastTriggerKind = this._source.inlineCompletions.map(this, v => v?.request?.context.triggerKind); + this.inlineCompletionsCount = derived(this, reader => { + if (this.lastTriggerKind.read(reader) === InlineCompletionTriggerKind.Explicit) { + return this._filteredInlineCompletionItems.read(reader).length; + } else { + return undefined; + } + }); + this._hasVisiblePeekWidgets = derived(this, reader => this._editorObs.openedPeekWidgets.read(reader) > 0); + this.state = derivedOpts<{ + kind: 'ghostText'; + edits: readonly TextReplacement[]; + primaryGhostText: GhostTextOrReplacement; + ghostTexts: readonly GhostTextOrReplacement[]; + suggestItem: SuggestItemInfo | undefined; + inlineCompletion: InlineCompletionItem | undefined; + } | { + kind: 'inlineEdit'; + edits: readonly TextReplacement[]; + inlineEdit: InlineEdit; + inlineCompletion: InlineEditItem; + cursorAtInlineEdit: IObservable; + } | undefined>({ + owner: this, + equalsFn: (a, b) => { + if (!a || !b) { return a === b; } + + if (a.kind === 'ghostText' && b.kind === 'ghostText') { + return ghostTextsOrReplacementsEqual(a.ghostTexts, b.ghostTexts) + && a.inlineCompletion === b.inlineCompletion + && a.suggestItem === b.suggestItem; + } else if (a.kind === 'inlineEdit' && b.kind === 'inlineEdit') { + return a.inlineEdit.equals(b.inlineEdit); + } + return false; + } + }, (reader) => { + const model = this.textModel; + + const item = this._inlineCompletionItems.read(reader); + const inlineEditResult = item?.inlineEdit; + if (inlineEditResult) { + if (this._hasVisiblePeekWidgets.read(reader)) { + return undefined; + } + let edit = inlineEditResult.getSingleTextEdit(); + edit = singleTextRemoveCommonPrefix(edit, model); + + const cursorAtInlineEdit = this.primaryPosition.map(cursorPos => LineRange.fromRangeInclusive(inlineEditResult.targetRange).addMargin(1, 1).contains(cursorPos.lineNumber)); + + const commands = inlineEditResult.source.inlineSuggestions.commands; + const inlineEdit = new InlineEdit(edit, commands ?? [], inlineEditResult); + + const edits = inlineEditResult.updatedEdit; + const e = edits ? TextEdit.fromStringEdit(edits, new TextModelText(this.textModel)).replacements : [edit]; + + return { kind: 'inlineEdit', inlineEdit, inlineCompletion: inlineEditResult, edits: e, cursorAtInlineEdit }; + } + + const suggestItem = this._selectedSuggestItem.read(reader); + if (suggestItem) { + const suggestCompletionEdit = singleTextRemoveCommonPrefix(suggestItem.getSingleTextEdit(), model); + const augmentation = this._computeAugmentation(suggestCompletionEdit, reader); + + const isSuggestionPreviewEnabled = this._suggestPreviewEnabled.read(reader); + if (!isSuggestionPreviewEnabled && !augmentation) { return undefined; } + + const fullEdit = augmentation?.edit ?? suggestCompletionEdit; + const fullEditPreviewLength = augmentation ? augmentation.edit.text.length - suggestCompletionEdit.text.length : 0; + + const mode = this._suggestPreviewMode.read(reader); + const positions = this._positions.read(reader); + const edits = [fullEdit, ...getSecondaryEdits(this.textModel, positions, fullEdit)]; + const ghostTexts = edits + .map((edit, idx) => computeGhostText(edit, model, mode, positions[idx], fullEditPreviewLength)) + .filter(isDefined); + const primaryGhostText = ghostTexts[0] ?? new GhostText(fullEdit.range.endLineNumber, []); + return { kind: 'ghostText', edits, primaryGhostText, ghostTexts, inlineCompletion: augmentation?.completion, suggestItem }; + } else { + if (!this._isActive.read(reader)) { return undefined; } + const inlineCompletion = this.selectedInlineCompletion.read(reader); + if (!inlineCompletion) { return undefined; } + + const replacement = inlineCompletion.getSingleTextEdit(); + const mode = this._inlineSuggestMode.read(reader); + const positions = this._positions.read(reader); + const edits = [replacement, ...getSecondaryEdits(this.textModel, positions, replacement)]; + const ghostTexts = edits + .map((edit, idx) => computeGhostText(edit, model, mode, positions[idx], 0)) + .filter(isDefined); + if (!ghostTexts[0]) { return undefined; } + return { kind: 'ghostText', edits, primaryGhostText: ghostTexts[0], ghostTexts, inlineCompletion, suggestItem: undefined }; + } + }); + this.status = derived(this, reader => { + if (this._source.loading.read(reader)) { return 'loading'; } + const s = this.state.read(reader); + if (s?.kind === 'ghostText') { return 'ghostText'; } + if (s?.kind === 'inlineEdit') { return 'inlineEdit'; } + return 'noSuggestion'; + }); + this.inlineCompletionState = derived(this, reader => { + const s = this.state.read(reader); + if (!s || s.kind !== 'ghostText') { + return undefined; + } + if (this._editorObs.inComposition.read(reader)) { + return undefined; + } + return s; + }); + this.inlineEditState = derived(this, reader => { + const s = this.state.read(reader); + if (!s || s.kind !== 'inlineEdit') { + return undefined; + } + return s; + }); + this.inlineEditAvailable = derived(this, reader => { + const s = this.inlineEditState.read(reader); + return !!s; + }); + this.warning = derived(this, reader => { + return this.inlineCompletionState.read(reader)?.inlineCompletion?.warning; + }); + this.ghostTexts = derivedOpts({ owner: this, equalsFn: ghostTextsOrReplacementsEqual }, reader => { + const v = this.inlineCompletionState.read(reader); + if (!v) { + return undefined; + } + return v.ghostTexts; + }); + this.primaryGhostText = derivedOpts({ owner: this, equalsFn: ghostTextOrReplacementEquals }, reader => { + const v = this.inlineCompletionState.read(reader); + if (!v) { + return undefined; + } + return v?.primaryGhostText; + }); + + this._jumpedToId = observableValue(this, undefined); + this._inAcceptFlow = observableValue(this, false); + this.inAcceptFlow = this._inAcceptFlow; + + // When the suggestion appeared, was it inside the view port or not + const appearedInsideViewport = derived(this, reader => { + const state = this.state.read(reader); + if (!state || !state.inlineCompletion) { + return false; + } + + const targetRange = state.inlineCompletion.targetRange; + const visibleRanges = this._editorObs.editor.getVisibleRanges(); + if (visibleRanges.length < 1) { + return false; + } + + const viewportRange = new Range(visibleRanges[0].startLineNumber, visibleRanges[0].startColumn, visibleRanges[visibleRanges.length - 1].endLineNumber, visibleRanges[visibleRanges.length - 1].endColumn); + return viewportRange.containsRange(targetRange); + }); + + this.showCollapsed = derived(this, reader => { + const state = this.state.read(reader); + if (!state || state.kind !== 'inlineEdit') { + return false; + } + + if (state.inlineCompletion.displayLocation) { + return false; + } + + const isCurrentModelVersion = state.inlineCompletion.updatedEditModelVersion === this._textModelVersionId.read(reader); + return (this._inlineEditsShowCollapsedEnabled.read(reader) || !isCurrentModelVersion) + && this._jumpedToId.read(reader) !== state.inlineCompletion.semanticId + && !this._inAcceptFlow.read(reader); + }); + this._tabShouldIndent = derived(this, reader => { + if (this._inAcceptFlow.read(reader)) { + return false; + } + + function isMultiLine(range: Range): boolean { + return range.startLineNumber !== range.endLineNumber; + } + + function getNonIndentationRange(model: ITextModel, lineNumber: number): Range { + const columnStart = model.getLineIndentColumn(lineNumber); + const lastNonWsColumn = model.getLineLastNonWhitespaceColumn(lineNumber); + const columnEnd = Math.max(lastNonWsColumn, columnStart); + return new Range(lineNumber, columnStart, lineNumber, columnEnd); + } + + const selections = this._editorObs.selections.read(reader); + return selections?.some(s => { + if (s.isEmpty()) { + return this.textModel.getLineLength(s.startLineNumber) === 0; + } else { + return isMultiLine(s) || s.containsRange(getNonIndentationRange(this.textModel, s.startLineNumber)); + } + }); + }); + this.tabShouldJumpToInlineEdit = derived(this, reader => { + if (this._tabShouldIndent.read(reader)) { + return false; + } + + const s = this.inlineEditState.read(reader); + if (!s) { + return false; + } + + if (this.showCollapsed.read(reader)) { + return true; + } + + if (this._inAcceptFlow.read(reader) && appearedInsideViewport.read(reader)) { + return false; + } + + return !s.cursorAtInlineEdit.read(reader); + }); + this.tabShouldAcceptInlineEdit = derived(this, reader => { + const s = this.inlineEditState.read(reader); + if (!s) { + return false; + } + if (this.showCollapsed.read(reader)) { + return false; + } + if (this._inAcceptFlow.read(reader) && appearedInsideViewport.read(reader)) { + return true; + } + if (s.inlineCompletion.targetRange.startLineNumber === this._editorObs.cursorLineNumber.read(reader)) { + return true; + } + if (this._jumpedToId.read(reader) === s.inlineCompletion.semanticId) { + return true; + } + if (this._tabShouldIndent.read(reader)) { + return false; + } + + return s.cursorAtInlineEdit.read(reader); + }); this._register(recomputeInitiallyAndOnChange(this._fetchInlineCompletionsPromise)); @@ -156,30 +585,9 @@ export class InlineCompletionsModel extends Disposable { this._didUndoInlineEdits.recomputeInitiallyAndOnChange(this._store); } - private _lastShownInlineCompletionInfo: { alternateTextModelVersionId: number; /* already freed! */ inlineCompletion: InlineSuggestionItem } | undefined = undefined; - private _lastAcceptedInlineCompletionInfo: { textModelVersionIdAfter: number; /* already freed! */ inlineCompletion: InlineSuggestionItem } | undefined = undefined; - private readonly _didUndoInlineEdits = derivedHandleChanges({ - owner: this, - changeTracker: { - createChangeSummary: () => ({ didUndo: false }), - handleChange: (ctx, changeSummary) => { - changeSummary.didUndo = ctx.didChange(this._textModelVersionId) && !!ctx.change?.isUndoing; - return true; - } - } - }, (reader, changeSummary) => { - const versionId = this._textModelVersionId.read(reader); - if (versionId !== null - && this._lastAcceptedInlineCompletionInfo - && this._lastAcceptedInlineCompletionInfo.textModelVersionIdAfter === versionId - 1 - && this._lastAcceptedInlineCompletionInfo.inlineCompletion.isInlineEdit - && changeSummary.didUndo - ) { - this._lastAcceptedInlineCompletionInfo = undefined; - return true; - } - return false; - }); + private _lastShownInlineCompletionInfo: { alternateTextModelVersionId: number; /* already freed! */ inlineCompletion: InlineSuggestionItem } | undefined; + private _lastAcceptedInlineCompletionInfo: { textModelVersionIdAfter: number; /* already freed! */ inlineCompletion: InlineSuggestionItem } | undefined; + private readonly _didUndoInlineEdits; public debugGetSelectedSuggestItem(): IObservable { return this._selectedSuggestItem; @@ -215,11 +623,7 @@ export class InlineCompletionsModel extends Disposable { }; } - private readonly _preserveCurrentCompletionReasons = new Set([ - VersionIdChangeReason.Redo, - VersionIdChangeReason.Undo, - VersionIdChangeReason.AcceptWord, - ]); + private readonly _preserveCurrentCompletionReasons; private _getReason(e: IModelContentChangedEvent | undefined): VersionIdChangeReason { if (e?.isUndoing) { return VersionIdChangeReason.Undo; } @@ -228,96 +632,9 @@ export class InlineCompletionsModel extends Disposable { return VersionIdChangeReason.Other; } - public readonly dontRefetchSignal = observableSignal(this); + public readonly dontRefetchSignal; - private readonly _fetchInlineCompletionsPromise = derivedHandleChanges({ - owner: this, - changeTracker: { - createChangeSummary: () => ({ - dontRefetch: false, - preserveCurrentCompletion: false, - inlineCompletionTriggerKind: InlineCompletionTriggerKind.Automatic, - onlyRequestInlineEdits: false, - shouldDebounce: true, - provider: undefined as InlineCompletionsProvider | undefined, - }), - handleChange: (ctx, changeSummary) => { - /** @description fetch inline completions */ - if (ctx.didChange(this._textModelVersionId) && this._preserveCurrentCompletionReasons.has(this._getReason(ctx.change))) { - changeSummary.preserveCurrentCompletion = true; - } else if (ctx.didChange(this._forceUpdateExplicitlySignal)) { - changeSummary.inlineCompletionTriggerKind = InlineCompletionTriggerKind.Explicit; - } else if (ctx.didChange(this.dontRefetchSignal)) { - changeSummary.dontRefetch = true; - } else if (ctx.didChange(this._onlyRequestInlineEditsSignal)) { - changeSummary.onlyRequestInlineEdits = true; - } else if (ctx.didChange(this._fetchSpecificProviderSignal)) { - changeSummary.provider = ctx.change; - } - return true; - }, - }, - }, (reader, changeSummary) => { - this._source.clearOperationOnTextModelChange.read(reader); // Make sure the clear operation runs before the fetch operation - this._noDelaySignal.read(reader); - this.dontRefetchSignal.read(reader); - this._onlyRequestInlineEditsSignal.read(reader); - this._forceUpdateExplicitlySignal.read(reader); - this._fetchSpecificProviderSignal.read(reader); - const shouldUpdate = (this._enabled.read(reader) && this._selectedSuggestItem.read(reader)) || this._isActive.read(reader); - if (!shouldUpdate) { - this._source.cancelUpdate(); - return undefined; - } - - this._textModelVersionId.read(reader); // Refetch on text change - - const suggestWidgetInlineCompletions = this._source.suggestWidgetInlineCompletions.get(); - const suggestItem = this._selectedSuggestItem.read(reader); - if (suggestWidgetInlineCompletions && !suggestItem) { - this._source.seedInlineCompletionsWithSuggestWidget(); - } - - const cursorPosition = this.primaryPosition.get(); - if (changeSummary.dontRefetch) { - return Promise.resolve(true); - } - - if (this._didUndoInlineEdits.read(reader) && changeSummary.inlineCompletionTriggerKind !== InlineCompletionTriggerKind.Explicit) { - transaction(tx => { - this._source.clear(tx); - }); - return undefined; - } - - let context: InlineCompletionContextWithoutUuid = { - triggerKind: changeSummary.inlineCompletionTriggerKind, - selectedSuggestionInfo: suggestItem?.toSelectedSuggestionInfo(), - includeInlineCompletions: !changeSummary.onlyRequestInlineEdits, - includeInlineEdits: this._inlineEditsEnabled.read(reader), - }; - - if (context.triggerKind === InlineCompletionTriggerKind.Automatic) { - if (this.textModel.getAlternativeVersionId() === this._lastShownInlineCompletionInfo?.alternateTextModelVersionId) { - // When undoing back to a version where an inline edit/completion was shown, - // we want to show an inline edit (or completion) again if it was originally an inline edit (or completion). - context = { - ...context, - includeInlineCompletions: !this._lastShownInlineCompletionInfo.inlineCompletion.isInlineEdit, - includeInlineEdits: this._lastShownInlineCompletionInfo.inlineCompletion.isInlineEdit, - }; - } - } - - const itemToPreserveCandidate = this.selectedInlineCompletion.get() ?? this._inlineCompletionItems.get()?.inlineEdit; - const itemToPreserve = changeSummary.preserveCurrentCompletion || itemToPreserveCandidate?.forwardStable - ? itemToPreserveCandidate : undefined; - const userJumpedToActiveCompletion = this._jumpedToId.map(jumpedTo => !!jumpedTo && jumpedTo === this._inlineCompletionItems.get()?.inlineEdit?.semanticId); - - const providers = changeSummary.provider ? [changeSummary.provider] : this._languageFeaturesService.inlineCompletionsProvider.all(this.textModel); - - return this._source.fetch(providers, cursorPosition, context, itemToPreserve?.identity, changeSummary.shouldDebounce, userJumpedToActiveCompletion, !!changeSummary.provider); - }); + private readonly _fetchInlineCompletionsPromise; public async trigger(tx?: ITransaction, options?: { onlyFetchInlineEdits?: boolean; noDelay?: boolean }): Promise { subtransaction(tx, tx => { @@ -358,193 +675,34 @@ export class InlineCompletionsModel extends Disposable { }); } - private readonly _inlineCompletionItems = derivedOpts({ owner: this }, reader => { - const c = this._source.inlineCompletions.read(reader); - if (!c) { return undefined; } - const cursorPosition = this.primaryPosition.read(reader); - let inlineEdit: InlineEditItem | undefined = undefined; - const visibleCompletions: InlineCompletionItem[] = []; - for (const completion of c.inlineCompletions) { - if (!completion.isInlineEdit) { - if (completion.isVisible(this.textModel, cursorPosition)) { - visibleCompletions.push(completion); - } - } else { - inlineEdit = completion; - } - } + private readonly _inlineCompletionItems; - if (visibleCompletions.length !== 0) { - // Don't show the inline edit if there is a visible completion - inlineEdit = undefined; - } + private readonly _filteredInlineCompletionItems; - return { - inlineCompletions: visibleCompletions, - inlineEdit, - }; - }); + public readonly selectedInlineCompletionIndex; - private readonly _filteredInlineCompletionItems = derivedOpts({ owner: this, equalsFn: itemsEquals() }, reader => { - const c = this._inlineCompletionItems.read(reader); - return c?.inlineCompletions ?? []; - }); + public readonly selectedInlineCompletion; - public readonly selectedInlineCompletionIndex = derived(this, (reader) => { - const selectedInlineCompletionId = this._selectedInlineCompletionId.read(reader); - const filteredCompletions = this._filteredInlineCompletionItems.read(reader); - const idx = this._selectedInlineCompletionId === undefined ? -1 - : filteredCompletions.findIndex(v => v.semanticId === selectedInlineCompletionId); - if (idx === -1) { - // Reset the selection so that the selection does not jump back when it appears again - this._selectedInlineCompletionId.set(undefined, undefined); - return 0; - } - return idx; - }); - - public readonly selectedInlineCompletion = derived(this, (reader) => { - const filteredCompletions = this._filteredInlineCompletionItems.read(reader); - const idx = this.selectedInlineCompletionIndex.read(reader); - return filteredCompletions[idx]; - }); - - public readonly activeCommands = derivedOpts({ owner: this, equalsFn: itemsEquals() }, - r => this.selectedInlineCompletion.read(r)?.source.inlineSuggestions.commands ?? [] - ); + public readonly activeCommands; public readonly lastTriggerKind: IObservable - = this._source.inlineCompletions.map(this, v => v?.request?.context.triggerKind); + ; - public readonly inlineCompletionsCount = derived(this, reader => { - if (this.lastTriggerKind.read(reader) === InlineCompletionTriggerKind.Explicit) { - return this._filteredInlineCompletionItems.read(reader).length; - } else { - return undefined; - } - }); + public readonly inlineCompletionsCount; - private readonly _hasVisiblePeekWidgets = derived(this, reader => this._editorObs.openedPeekWidgets.read(reader) > 0); + private readonly _hasVisiblePeekWidgets; - public readonly state = derivedOpts<{ - kind: 'ghostText'; - edits: readonly SingleTextEdit[]; - primaryGhostText: GhostTextOrReplacement; - ghostTexts: readonly GhostTextOrReplacement[]; - suggestItem: SuggestItemInfo | undefined; - inlineCompletion: InlineCompletionItem | undefined; - } | { - kind: 'inlineEdit'; - edits: readonly SingleTextEdit[]; - inlineEdit: InlineEdit; - inlineCompletion: InlineEditItem; - cursorAtInlineEdit: IObservable; - } | undefined>({ - owner: this, - equalsFn: (a, b) => { - if (!a || !b) { return a === b; } + public readonly state; - if (a.kind === 'ghostText' && b.kind === 'ghostText') { - return ghostTextsOrReplacementsEqual(a.ghostTexts, b.ghostTexts) - && a.inlineCompletion === b.inlineCompletion - && a.suggestItem === b.suggestItem; - } else if (a.kind === 'inlineEdit' && b.kind === 'inlineEdit') { - return a.inlineEdit.equals(b.inlineEdit); - } - return false; - } - }, (reader) => { - const model = this.textModel; + public readonly status; - const item = this._inlineCompletionItems.read(reader); - const inlineEditResult = item?.inlineEdit; - if (inlineEditResult) { - if (this._hasVisiblePeekWidgets.read(reader)) { - return undefined; - } - let edit = inlineEditResult.getSingleTextEdit(); - edit = singleTextRemoveCommonPrefix(edit, model); + public readonly inlineCompletionState; - const cursorAtInlineEdit = this.primaryPosition.map(cursorPos => LineRange.fromRangeInclusive(inlineEditResult.targetRange).addMargin(1, 1).contains(cursorPos.lineNumber)); + public readonly inlineEditState; - const commands = inlineEditResult.source.inlineSuggestions.commands; - const inlineEdit = new InlineEdit(edit, commands ?? [], inlineEditResult); + public readonly inlineEditAvailable; - const edits = inlineEditResult.updatedEdit; - const e = edits ? TextEdit.fromOffsetEdit(edits, new TextModelText(this.textModel)).edits : [edit]; - - return { kind: 'inlineEdit', inlineEdit, inlineCompletion: inlineEditResult, edits: e, cursorAtInlineEdit }; - } - - const suggestItem = this._selectedSuggestItem.read(reader); - if (suggestItem) { - const suggestCompletionEdit = singleTextRemoveCommonPrefix(suggestItem.getSingleTextEdit(), model); - const augmentation = this._computeAugmentation(suggestCompletionEdit, reader); - - const isSuggestionPreviewEnabled = this._suggestPreviewEnabled.read(reader); - if (!isSuggestionPreviewEnabled && !augmentation) { return undefined; } - - const fullEdit = augmentation?.edit ?? suggestCompletionEdit; - const fullEditPreviewLength = augmentation ? augmentation.edit.text.length - suggestCompletionEdit.text.length : 0; - - const mode = this._suggestPreviewMode.read(reader); - const positions = this._positions.read(reader); - const edits = [fullEdit, ...getSecondaryEdits(this.textModel, positions, fullEdit)]; - const ghostTexts = edits - .map((edit, idx) => computeGhostText(edit, model, mode, positions[idx], fullEditPreviewLength)) - .filter(isDefined); - const primaryGhostText = ghostTexts[0] ?? new GhostText(fullEdit.range.endLineNumber, []); - return { kind: 'ghostText', edits, primaryGhostText, ghostTexts, inlineCompletion: augmentation?.completion, suggestItem }; - } else { - if (!this._isActive.read(reader)) { return undefined; } - const inlineCompletion = this.selectedInlineCompletion.read(reader); - if (!inlineCompletion) { return undefined; } - - const replacement = inlineCompletion.getSingleTextEdit(); - const mode = this._inlineSuggestMode.read(reader); - const positions = this._positions.read(reader); - const edits = [replacement, ...getSecondaryEdits(this.textModel, positions, replacement)]; - const ghostTexts = edits - .map((edit, idx) => computeGhostText(edit, model, mode, positions[idx], 0)) - .filter(isDefined); - if (!ghostTexts[0]) { return undefined; } - return { kind: 'ghostText', edits, primaryGhostText: ghostTexts[0], ghostTexts, inlineCompletion, suggestItem: undefined }; - } - }); - - public readonly status = derived(this, reader => { - if (this._source.loading.read(reader)) { return 'loading'; } - const s = this.state.read(reader); - if (s?.kind === 'ghostText') { return 'ghostText'; } - if (s?.kind === 'inlineEdit') { return 'inlineEdit'; } - return 'noSuggestion'; - }); - - public readonly inlineCompletionState = derived(this, reader => { - const s = this.state.read(reader); - if (!s || s.kind !== 'ghostText') { - return undefined; - } - if (this._editorObs.inComposition.read(reader)) { - return undefined; - } - return s; - }); - - public readonly inlineEditState = derived(this, reader => { - const s = this.state.read(reader); - if (!s || s.kind !== 'inlineEdit') { - return undefined; - } - return s; - }); - - public readonly inlineEditAvailable = derived(this, reader => { - const s = this.inlineEditState.read(reader); - return !!s; - }); - - private _computeAugmentation(suggestCompletion: SingleTextEdit, reader: IReader | undefined) { + private _computeAugmentation(suggestCompletion: TextReplacement, reader: IReader | undefined) { const model = this.textModel; const suggestWidgetInlineCompletions = this._source.suggestWidgetInlineCompletions.read(reader); const candidateInlineCompletions = suggestWidgetInlineCompletions @@ -564,105 +722,19 @@ export class InlineCompletionsModel extends Disposable { return augmentedCompletion; } - public readonly warning = derived(this, reader => { - return this.inlineCompletionState.read(reader)?.inlineCompletion?.warning; - }); + public readonly warning; - public readonly ghostTexts = derivedOpts({ owner: this, equalsFn: ghostTextsOrReplacementsEqual }, reader => { - const v = this.inlineCompletionState.read(reader); - if (!v) { - return undefined; - } - return v.ghostTexts; - }); + public readonly ghostTexts; - public readonly primaryGhostText = derivedOpts({ owner: this, equalsFn: ghostTextOrReplacementEquals }, reader => { - const v = this.inlineCompletionState.read(reader); - if (!v) { - return undefined; - } - return v?.primaryGhostText; - }); + public readonly primaryGhostText; - public readonly showCollapsed = derived(this, reader => { - const state = this.state.read(reader); - if (!state || state.kind !== 'inlineEdit') { - return false; - } + public readonly showCollapsed; - if (state.inlineCompletion.displayLocation) { - return false; - } + private readonly _tabShouldIndent; - const isCurrentModelVersion = state.inlineCompletion.updatedEditModelVersion === this._textModelVersionId.read(reader); - return (this._inlineEditsShowCollapsedEnabled.read(reader) || !isCurrentModelVersion) - && this._jumpedToId.read(reader) !== state.inlineCompletion.semanticId - && !this._inAcceptFlow.read(reader); - }); + public readonly tabShouldJumpToInlineEdit; - private readonly _tabShouldIndent = derived(this, reader => { - if (this._inAcceptFlow.read(reader)) { - return false; - } - - function isMultiLine(range: Range): boolean { - return range.startLineNumber !== range.endLineNumber; - } - - function getNonIndentationRange(model: ITextModel, lineNumber: number): Range { - const columnStart = model.getLineIndentColumn(lineNumber); - const lastNonWsColumn = model.getLineLastNonWhitespaceColumn(lineNumber); - const columnEnd = Math.max(lastNonWsColumn, columnStart); - return new Range(lineNumber, columnStart, lineNumber, columnEnd); - } - - const selections = this._editorObs.selections.read(reader); - return selections?.some(s => { - if (s.isEmpty()) { - return this.textModel.getLineLength(s.startLineNumber) === 0; - } else { - return isMultiLine(s) || s.containsRange(getNonIndentationRange(this.textModel, s.startLineNumber)); - } - }); - }); - - public readonly tabShouldJumpToInlineEdit = derived(this, reader => { - if (this._tabShouldIndent.read(reader)) { - return false; - } - - const s = this.inlineEditState.read(reader); - if (!s) { - return false; - } - - if (this.showCollapsed.read(reader)) { - return true; - } - - return !s.cursorAtInlineEdit.read(reader); - }); - - public readonly tabShouldAcceptInlineEdit = derived(this, reader => { - const s = this.inlineEditState.read(reader); - if (!s) { - return false; - } - if (this.showCollapsed.read(reader)) { - return false; - } - if (s.inlineCompletion.targetRange.startLineNumber === this._editorObs.cursorLineNumber.read(reader)) { - return true; - } - if (this._jumpedToId.read(reader) === s.inlineCompletion.semanticId) { - return true; - } - if (this._tabShouldIndent.read(reader)) { - return false; - } - - return s.cursorAtInlineEdit.read(reader); - }); + public readonly tabShouldAcceptInlineEdit; private async _deltaSelectedInlineCompletionIndex(delta: 1 | -1): Promise { await this.triggerExplicitly(); @@ -845,7 +917,7 @@ export class InlineCompletionsModel extends Disposable { editor.pushUndoStop(); const replaceRange = Range.fromPositions(cursorPosition, ghostTextPos); const newText = editor.getModel()!.getValueInRange(replaceRange) + partialGhostTextVal; - const primaryEdit = new SingleTextEdit(replaceRange, newText); + const primaryEdit = new TextReplacement(replaceRange, newText); const edits = [primaryEdit, ...getSecondaryEdits(this.textModel, positions, primaryEdit)]; const selections = getEndPositionsAfterApplying(edits).map(p => Selection.fromPositions(p)); TextModelChangeRecorder.editWithMetadata(this._getMetadata(completion, type), () => { @@ -892,9 +964,9 @@ export class InlineCompletionsModel extends Disposable { }; } - private readonly _jumpedToId = observableValue(this, undefined); - private readonly _inAcceptFlow = observableValue(this, false); - public readonly inAcceptFlow: IObservable = this._inAcceptFlow; + private readonly _jumpedToId; + private readonly _inAcceptFlow; + public readonly inAcceptFlow: IObservable; public jump(): void { const s = this.inlineEditState.get(); @@ -937,7 +1009,7 @@ export enum VersionIdChangeReason { Other, } -export function getSecondaryEdits(textModel: ITextModel, positions: readonly Position[], primaryEdit: SingleTextEdit): SingleTextEdit[] { +export function getSecondaryEdits(textModel: ITextModel, positions: readonly Position[], primaryEdit: TextReplacement): TextReplacement[] { if (positions.length === 1) { // No secondary cursor positions return []; @@ -965,7 +1037,7 @@ export function getSecondaryEdits(textModel: ITextModel, positions: readonly Pos ); const l = commonPrefixLength(replacedTextAfterPrimaryCursor, textAfterSecondaryCursor); const range = Range.fromPositions(pos, pos.delta(0, l)); - return new SingleTextEdit(range, secondaryEditText); + return new TextReplacement(range, secondaryEditText); }); } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts index c3ae7b8f412..55d598409df 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts @@ -16,12 +16,12 @@ import { IConfigurationService } from '../../../../../platform/configuration/com import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; import { observableConfigValue } from '../../../../../platform/observable/common/platformObservableUtils.js'; -import { OffsetEdit } from '../../../../common/core/edits/offsetEdit.js'; +import { StringEdit } from '../../../../common/core/edits/stringEdit.js'; import { Position } from '../../../../common/core/position.js'; import { InlineCompletionEndOfLifeReasonKind, InlineCompletionTriggerKind, InlineCompletionsProvider } from '../../../../common/languages.js'; import { ILanguageConfigurationService } from '../../../../common/languages/languageConfigurationRegistry.js'; import { ITextModel } from '../../../../common/model.js'; -import { OffsetEdits } from '../../../../common/model/textModelOffsetEdit.js'; +import { offsetEditFromContentChanges } from '../../../../common/model/textModelStringEdit.js'; import { IFeatureDebounceInformation } from '../../../../common/services/languageFeatureDebounce.js'; import { IModelContentChangedEvent } from '../../../../common/textModelEvents.js'; import { formatRecordableLogEntry, IRecordableEditorLogEntry, IRecordableLogEntry, StructuredLogger } from '../structuredLogger.js'; @@ -32,47 +32,16 @@ import { InlineCompletionContextWithoutUuid, InlineCompletionProviderResult, pro export class InlineCompletionsSource extends Disposable { private static _requestId = 0; - private readonly _updateOperation = this._register(new MutableDisposable()); + private readonly _updateOperation; - private readonly _loggingEnabled = observableConfigValue('editor.inlineSuggest.logFetch', false, this._configurationService).recomputeInitiallyAndOnChange(this._store); + private readonly _loggingEnabled; - private readonly _structuredFetchLogger = this._register(this._instantiationService.createInstance(StructuredLogger.cast< - { kind: 'start'; requestId: number; context: unknown } & IRecordableEditorLogEntry - | { kind: 'end'; error: any; durationMs: number; result: unknown; requestId: number } & IRecordableLogEntry - >(), - 'editor.inlineSuggest.logFetch.commandId' - )); + private readonly _structuredFetchLogger; - private readonly _state = observableReducerSettable(this, { - initial: () => ({ - inlineCompletions: InlineCompletionsState.createEmpty(), - suggestWidgetInlineCompletions: InlineCompletionsState.createEmpty(), - }), - disposeFinal: (values) => { - values.inlineCompletions.dispose(); - values.suggestWidgetInlineCompletions.dispose(); - }, - changeTracker: recordChanges({ versionId: this._versionId }), - update: (reader, previousValue, changes) => { - const edit = OffsetEdit.join(changes.changes.map(c => c.change ? OffsetEdits.fromContentChanges(c.change.changes) : OffsetEdit.empty).filter(isDefined)); + private readonly _state; - if (edit.isEmpty) { - return previousValue; - } - try { - return { - inlineCompletions: previousValue.inlineCompletions.createStateWithAppliedEdit(edit, this._textModel), - suggestWidgetInlineCompletions: previousValue.suggestWidgetInlineCompletions.createStateWithAppliedEdit(edit, this._textModel), - }; - } finally { - previousValue.inlineCompletions.dispose(); - previousValue.suggestWidgetInlineCompletions.dispose(); - } - } - }); - - public readonly inlineCompletions = this._state.map(this, v => v.inlineCompletions); - public readonly suggestWidgetInlineCompletions = this._state.map(this, v => v.suggestWidgetInlineCompletions); + public readonly inlineCompletions; + public readonly suggestWidgetInlineCompletions; constructor( private readonly _textModel: ITextModel, @@ -84,15 +53,55 @@ export class InlineCompletionsSource extends Disposable { @IInstantiationService private readonly _instantiationService: IInstantiationService, ) { super(); + this._updateOperation = this._register(new MutableDisposable()); + this._loggingEnabled = observableConfigValue('editor.inlineSuggest.logFetch', false, this._configurationService).recomputeInitiallyAndOnChange(this._store); + this._structuredFetchLogger = this._register(this._instantiationService.createInstance(StructuredLogger.cast< + { kind: 'start'; requestId: number; context: unknown } & IRecordableEditorLogEntry + | { kind: 'end'; error: any; durationMs: number; result: unknown; requestId: number } & IRecordableLogEntry + >(), + 'editor.inlineSuggest.logFetch.commandId' + )); + this._state = observableReducerSettable(this, { + initial: () => ({ + inlineCompletions: InlineCompletionsState.createEmpty(), + suggestWidgetInlineCompletions: InlineCompletionsState.createEmpty(), + }), + disposeFinal: (values) => { + values.inlineCompletions.dispose(); + values.suggestWidgetInlineCompletions.dispose(); + }, + changeTracker: recordChanges({ versionId: this._versionId }), + update: (reader, previousValue, changes) => { + const edit = StringEdit.compose(changes.changes.map(c => c.change ? offsetEditFromContentChanges(c.change.changes) : StringEdit.empty).filter(isDefined)); + + if (edit.isEmpty()) { + return previousValue; + } + try { + return { + inlineCompletions: previousValue.inlineCompletions.createStateWithAppliedEdit(edit, this._textModel), + suggestWidgetInlineCompletions: previousValue.suggestWidgetInlineCompletions.createStateWithAppliedEdit(edit, this._textModel), + }; + } finally { + previousValue.inlineCompletions.dispose(); + previousValue.suggestWidgetInlineCompletions.dispose(); + } + } + }); + this.inlineCompletions = this._state.map(this, v => v.inlineCompletions); + this.suggestWidgetInlineCompletions = this._state.map(this, v => v.suggestWidgetInlineCompletions); + this.clearOperationOnTextModelChange = derived(this, reader => { + this._versionId.read(reader); + this._updateOperation.clear(); + return undefined; // always constant + }); + this._loadingCount = observableValue(this, 0); + this.loading = this._loadingCount.map(this, v => v > 0); this.clearOperationOnTextModelChange.recomputeInitiallyAndOnChange(this._store); } - public readonly clearOperationOnTextModelChange = derived(this, reader => { - this._versionId.read(reader); - this._updateOperation.clear(); - return undefined; // always constant - }); + public readonly clearOperationOnTextModelChange; private _log(entry: { sourceId: string; kind: 'start'; requestId: number; context: unknown } & IRecordableEditorLogEntry @@ -104,8 +113,8 @@ export class InlineCompletionsSource extends Disposable { this._structuredFetchLogger.log(entry); } - private readonly _loadingCount = observableValue(this, 0); - public readonly loading = this._loadingCount.map(this, v => v > 0); + private readonly _loadingCount; + public readonly loading; public fetch(providers: InlineCompletionsProvider[], position: Position, context: InlineCompletionContextWithoutUuid, activeInlineCompletion: InlineSuggestionIdentity | undefined, withDebounce: boolean, userJumpedToActiveCompletion: IObservable, providerhasChangedCompletion: boolean): Promise { const request = new UpdateRequest(position, context, this._textModel.getVersionId()); @@ -333,7 +342,7 @@ class InlineCompletionsState extends Disposable { /** * Applies the edit on the state. */ - public createStateWithAppliedEdit(edit: OffsetEdit, textModel: ITextModel): InlineCompletionsState { + public createStateWithAppliedEdit(edit: StringEdit, textModel: ITextModel): InlineCompletionsState { const newInlineCompletions = this.inlineCompletions.map(i => i.withEdit(edit, textModel)).filter(isDefined); return new InlineCompletionsState(newInlineCompletions, this.request); } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineEdit.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineEdit.ts index 03baa11ae28..45cecd09b26 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineEdit.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineEdit.ts @@ -3,13 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { SingleTextEdit } from '../../../../common/core/edits/textEdit.js'; +import { TextReplacement } from '../../../../common/core/edits/textEdit.js'; import { Command } from '../../../../common/languages.js'; import { InlineSuggestionItem } from './inlineSuggestionItem.js'; export class InlineEdit { constructor( - public readonly edit: SingleTextEdit, + public readonly edit: TextReplacement, public readonly commands: readonly Command[], public readonly inlineCompletion: InlineSuggestionItem, ) { } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineSuggestionItem.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineSuggestionItem.ts index 58f1e932bab..f97e2d888ea 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineSuggestionItem.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineSuggestionItem.ts @@ -9,13 +9,14 @@ import { observableSignal, IObservable } from '../../../../../base/common/observ import { commonPrefixLength, commonSuffixLength, splitLines } from '../../../../../base/common/strings.js'; import { ICommandService } from '../../../../../platform/commands/common/commands.js'; import { ISingleEditOperation } from '../../../../common/core/editOperation.js'; -import { applyEditsToRanges, OffsetEdit, SingleOffsetEdit } from '../../../../common/core/edits/offsetEdit.js'; -import { OffsetRange } from '../../../../common/core/offsetRange.js'; +import { applyEditsToRanges, StringEdit, StringReplacement } from '../../../../common/core/edits/stringEdit.js'; +import { OffsetRange } from '../../../../common/core/ranges/offsetRange.js'; import { Position } from '../../../../common/core/position.js'; -import { getPositionOffsetTransformerFromTextModel, PositionOffsetTransformerBase } from '../../../../common/core/positionToOffset.js'; +import { getPositionOffsetTransformerFromTextModel, PositionOffsetTransformerBase } from '../../../../common/core/text/positionToOffset.js'; import { Range } from '../../../../common/core/range.js'; -import { SingleTextEdit, StringText, TextEdit } from '../../../../common/core/edits/textEdit.js'; -import { TextLength } from '../../../../common/core/textLength.js'; +import { TextReplacement, TextEdit } from '../../../../common/core/edits/textEdit.js'; +import { StringText } from '../../../../common/core/text/abstractText.js'; +import { TextLength } from '../../../../common/core/text/textLength.js'; import { linesDiffComputers } from '../../../../common/diff/linesDiffComputers.js'; import { InlineCompletion, InlineCompletionTriggerKind, Command, InlineCompletionWarning, PartialAcceptInfo, InlineCompletionEndOfLifeReason } from '../../../../common/languages.js'; import { ITextModel, EndOfLinePreference } from '../../../../common/model.js'; @@ -78,9 +79,9 @@ abstract class InlineSuggestionItemBase { private get _sourceInlineCompletion(): InlineCompletion { return this._data.sourceInlineCompletion; } - public abstract getSingleTextEdit(): SingleTextEdit; + public abstract getSingleTextEdit(): TextReplacement; - public abstract withEdit(userEdit: OffsetEdit, textModel: ITextModel): InlineSuggestionItem | undefined; + public abstract withEdit(userEdit: StringEdit, textModel: ITextModel): InlineSuggestionItem | undefined; public abstract withIdentity(identity: InlineSuggestionIdentity): InlineSuggestionItem; public abstract canBeReused(model: ITextModel, position: Position): boolean; @@ -161,7 +162,7 @@ class InlineSuggestDisplayLocation implements IDisplayLocation { public readonly label: string, ) { } - public withEdit(edit: OffsetEdit, positionOffsetTransformer: PositionOffsetTransformerBase): InlineSuggestDisplayLocation | undefined { + public withEdit(edit: StringEdit, positionOffsetTransformer: PositionOffsetTransformerBase): InlineSuggestDisplayLocation | undefined { const newOffsetRange = applyEditsToRanges([this._offsetRange], edit)[0]; if (!newOffsetRange || newOffsetRange.length !== this._offsetRange.length) { return undefined; @@ -183,8 +184,8 @@ export class InlineCompletionItem extends InlineSuggestionItemBase { textModel: ITextModel, ): InlineCompletionItem { const identity = new InlineSuggestionIdentity(); - const textEdit = new SingleTextEdit(data.range, data.insertText); - const edit = getPositionOffsetTransformerFromTextModel(textModel).getSingleOffsetEdit(textEdit); + const textEdit = new TextReplacement(data.range, data.insertText); + const edit = getPositionOffsetTransformerFromTextModel(textModel).getStringReplacement(textEdit); const displayLocation = data.displayLocation ? InlineSuggestDisplayLocation.create(data.displayLocation, textModel) : undefined; return new InlineCompletionItem(edit, textEdit, data.range, data.snippetInfo, data.additionalTextEdits, data, identity, displayLocation); @@ -193,8 +194,8 @@ export class InlineCompletionItem extends InlineSuggestionItemBase { public readonly isInlineEdit = false; private constructor( - private readonly _edit: SingleOffsetEdit, - private readonly _textEdit: SingleTextEdit, + private readonly _edit: StringReplacement, + private readonly _textEdit: TextReplacement, private readonly _originalRange: Range, public readonly snippetInfo: SnippetInfo | undefined, public readonly additionalTextEdits: readonly ISingleEditOperation[], @@ -206,7 +207,7 @@ export class InlineCompletionItem extends InlineSuggestionItemBase { super(data, identity, displayLocation); } - override getSingleTextEdit(): SingleTextEdit { return this._textEdit; } + override getSingleTextEdit(): TextReplacement { return this._textEdit; } override withIdentity(identity: InlineSuggestionIdentity): InlineCompletionItem { return new InlineCompletionItem( @@ -221,12 +222,12 @@ export class InlineCompletionItem extends InlineSuggestionItemBase { ); } - override withEdit(textModelEdit: OffsetEdit, textModel: ITextModel): InlineCompletionItem | undefined { + override withEdit(textModelEdit: StringEdit, textModel: ITextModel): InlineCompletionItem | undefined { const newEditRange = applyEditsToRanges([this._edit.replaceRange], textModelEdit); if (newEditRange.length === 0) { return undefined; } - const newEdit = new SingleOffsetEdit(newEditRange[0], this._textEdit.text); + const newEdit = new StringReplacement(newEditRange[0], this._textEdit.text); const positionOffsetTransformer = getPositionOffsetTransformerFromTextModel(textModel); const newTextEdit = positionOffsetTransformer.getSingleTextEdit(newEdit); @@ -305,13 +306,13 @@ export class InlineEditItem extends InlineSuggestionItemBase { data: InlineSuggestData, textModel: ITextModel, ): InlineEditItem { - const offsetEdit = getOffsetEdit(textModel, data.range, data.insertText); + const offsetEdit = getStringEdit(textModel, data.range, data.insertText); const text = new TextModelText(textModel); - const textEdit = TextEdit.fromOffsetEdit(offsetEdit, text); - const singleTextEdit = textEdit.toSingle(text); + const textEdit = TextEdit.fromStringEdit(offsetEdit, text); + const singleTextEdit = textEdit.toReplacement(text); const identity = new InlineSuggestionIdentity(); - const edits = offsetEdit.edits.map(edit => { + const edits = offsetEdit.replacements.map(edit => { const replacedRange = Range.fromPositions(textModel.getPositionAt(edit.replaceRange.start), textModel.getPositionAt(edit.replaceRange.endExclusive)); const replacedText = textModel.getValueInRange(replacedRange); return SingleUpdatedNextEdit.create(edit, replacedText); @@ -325,8 +326,8 @@ export class InlineEditItem extends InlineSuggestionItemBase { public readonly isInlineEdit = true; private constructor( - private readonly _edit: OffsetEdit, - private readonly _textEdit: SingleTextEdit, + private readonly _edit: StringEdit, + private readonly _textEdit: TextReplacement, data: InlineSuggestData, @@ -340,9 +341,9 @@ export class InlineEditItem extends InlineSuggestionItemBase { } public get updatedEditModelVersion(): number { return this._inlineEditModelVersion; } - public get updatedEdit(): OffsetEdit { return this._edit; } + public get updatedEdit(): StringEdit { return this._edit; } - override getSingleTextEdit(): SingleTextEdit { + override getSingleTextEdit(): TextReplacement { return this._textEdit; } @@ -364,12 +365,12 @@ export class InlineEditItem extends InlineSuggestionItemBase { return this._lastChangePartOfInlineEdit && this.updatedEditModelVersion === model.getVersionId(); } - override withEdit(textModelChanges: OffsetEdit, textModel: ITextModel): InlineEditItem | undefined { + override withEdit(textModelChanges: StringEdit, textModel: ITextModel): InlineEditItem | undefined { const edit = this._applyTextModelChanges(textModelChanges, this._edits, textModel); return edit; } - private _applyTextModelChanges(textModelChanges: OffsetEdit, edits: readonly SingleUpdatedNextEdit[], textModel: ITextModel): InlineEditItem | undefined { + private _applyTextModelChanges(textModelChanges: StringEdit, edits: readonly SingleUpdatedNextEdit[], textModel: ITextModel): InlineEditItem | undefined { edits = edits.map(innerEdit => innerEdit.applyTextModelChanges(textModelChanges)); if (edits.some(edit => edit.edit === undefined)) { @@ -393,9 +394,9 @@ export class InlineEditItem extends InlineSuggestionItemBase { return undefined; // the completion has been typed by the user } - const newEdit = new OffsetEdit(edits.map(edit => edit.edit!)); + const newEdit = new StringEdit(edits.map(edit => edit.edit!)); const positionOffsetTransformer = getPositionOffsetTransformerFromTextModel(textModel); - const newTextEdit = positionOffsetTransformer.getTextEdit(newEdit).toSingle(new TextModelText(textModel)); + const newTextEdit = positionOffsetTransformer.getTextEdit(newEdit).toReplacement(new TextModelText(textModel)); let newDisplayLocation = this.displayLocation; if (newDisplayLocation) { @@ -418,7 +419,7 @@ export class InlineEditItem extends InlineSuggestionItemBase { } } -function getOffsetEdit(textModel: ITextModel, editRange: Range, replaceText: string): OffsetEdit { +function getStringEdit(textModel: ITextModel, editRange: Range, replaceText: string): StringEdit { const eol = textModel.getEOL(); const editOriginalText = textModel.getValueInRange(editRange); const editReplaceText = replaceText.replace(/\r\n|\r|\n/g, eol); @@ -444,13 +445,13 @@ function getOffsetEdit(textModel: ITextModel, editRange: Range, replaceText: str const modifiedText = new StringText(editReplaceText); - const offsetEdit = new OffsetEdit( + const offsetEdit = new StringEdit( innerChanges.map(c => { const rangeInModel = addRangeToPos(editRange.getStartPosition(), c.originalRange); const originalRange = getPositionOffsetTransformerFromTextModel(textModel).getOffsetRange(rangeInModel); const replaceText = modifiedText.getValueOfRange(c.modifiedRange); - const edit = new SingleOffsetEdit(originalRange, replaceText); + const edit = new StringReplacement(originalRange, replaceText); const originalText = textModel.getValueInRange(rangeInModel); return reshapeEdit(edit, originalText, innerChanges.length, textModel); @@ -462,7 +463,7 @@ function getOffsetEdit(textModel: ITextModel, editRange: Range, replaceText: str class SingleUpdatedNextEdit { public static create( - edit: SingleOffsetEdit, + edit: StringReplacement, replacedText: string, ): SingleUpdatedNextEdit { const prefixLength = commonPrefixLength(edit.newText, replacedText); @@ -475,7 +476,7 @@ class SingleUpdatedNextEdit { public get lastChangeUpdatedEdit() { return this._lastChangeUpdatedEdit; } constructor( - private _edit: SingleOffsetEdit | undefined, + private _edit: StringReplacement | undefined, private _trimmedNewText: string, private _prefixLength: number, private _suffixLength: number, @@ -483,7 +484,7 @@ class SingleUpdatedNextEdit { ) { } - public applyTextModelChanges(textModelChanges: OffsetEdit) { + public applyTextModelChanges(textModelChanges: StringEdit) { const c = this._clone(); c._applyTextModelChanges(textModelChanges); return c; @@ -499,7 +500,7 @@ class SingleUpdatedNextEdit { ); } - private _applyTextModelChanges(textModelChanges: OffsetEdit) { + private _applyTextModelChanges(textModelChanges: StringEdit) { this._lastChangeUpdatedEdit = false; if (!this._edit) { @@ -516,7 +517,7 @@ class SingleUpdatedNextEdit { this._lastChangeUpdatedEdit = result.editHasChanged; } - private _applyChanges(edit: SingleOffsetEdit, textModelChanges: OffsetEdit): { edit: SingleOffsetEdit; editHasChanged: boolean } | undefined { + private _applyChanges(edit: StringReplacement, textModelChanges: StringEdit): { edit: StringReplacement; editHasChanged: boolean } | undefined { let editStart = edit.replaceRange.start; let editEnd = edit.replaceRange.endExclusive; let editReplaceText = edit.newText; @@ -524,8 +525,8 @@ class SingleUpdatedNextEdit { const shouldPreserveEditShape = this._prefixLength > 0 || this._suffixLength > 0; - for (let i = textModelChanges.edits.length - 1; i >= 0; i--) { - const change = textModelChanges.edits[i]; + for (let i = textModelChanges.replacements.length - 1; i >= 0; i--) { + const change = textModelChanges.replacements[i]; // INSERTIONS (only support inserting at start of edit) const isInsertion = change.newText.length > 0 && change.replaceRange.isEmpty; @@ -581,18 +582,18 @@ class SingleUpdatedNextEdit { // the resulting edit is a noop as the original and new text are the same if (this._trimmedNewText.length === 0 && editStart + this._prefixLength === editEnd - this._suffixLength) { - return { edit: new SingleOffsetEdit(new OffsetRange(editStart + this._prefixLength, editStart + this._prefixLength), ''), editHasChanged: true }; + return { edit: new StringReplacement(new OffsetRange(editStart + this._prefixLength, editStart + this._prefixLength), ''), editHasChanged: true }; } - return { edit: new SingleOffsetEdit(new OffsetRange(editStart, editEnd), editReplaceText), editHasChanged }; + return { edit: new StringReplacement(new OffsetRange(editStart, editEnd), editReplaceText), editHasChanged }; } } -function reshapeEdit(edit: SingleOffsetEdit, originalText: string, totalInnerEdits: number, textModel: ITextModel): SingleOffsetEdit { +function reshapeEdit(edit: StringReplacement, originalText: string, totalInnerEdits: number, textModel: ITextModel): StringReplacement { // TODO: EOL are not properly trimmed by the diffAlgorithm #12680 const eol = textModel.getEOL(); if (edit.newText.endsWith(eol) && originalText.endsWith(eol)) { - edit = new SingleOffsetEdit(edit.replaceRange.deltaEnd(-eol.length), edit.newText.slice(0, -eol.length)); + edit = new StringReplacement(edit.replaceRange.deltaEnd(-eol.length), edit.newText.slice(0, -eol.length)); } // INSERTION @@ -610,19 +611,19 @@ function reshapeEdit(edit: SingleOffsetEdit, originalText: string, totalInnerEdi // reshape it back to an insertion if (prefixLength + suffixLength === originalText.length) { - return new SingleOffsetEdit(edit.replaceRange.deltaStart(prefixLength).deltaEnd(-suffixLength), edit.newText.substring(prefixLength, edit.newText.length - suffixLength)); + return new StringReplacement(edit.replaceRange.deltaStart(prefixLength).deltaEnd(-suffixLength), edit.newText.substring(prefixLength, edit.newText.length - suffixLength)); } // reshape it back to a deletion if (prefixLength + suffixLength === edit.newText.length) { - return new SingleOffsetEdit(edit.replaceRange.deltaStart(prefixLength).deltaEnd(-suffixLength), ''); + return new StringReplacement(edit.replaceRange.deltaStart(prefixLength).deltaEnd(-suffixLength), ''); } } return edit; } -function reshapeMultiLineInsertion(edit: SingleOffsetEdit, textModel: ITextModel): SingleOffsetEdit { +function reshapeMultiLineInsertion(edit: StringReplacement, textModel: ITextModel): StringReplacement { if (!edit.replaceRange.isEmpty) { throw new BugIndicatingError('Unexpected original range'); } @@ -639,7 +640,7 @@ function reshapeMultiLineInsertion(edit: SingleOffsetEdit, textModel: ITextModel // If the insertion ends with a new line and is inserted at the start of a line which has text, // we move the insertion to the end of the previous line if possible if (startColumn === 1 && startLineNumber > 1 && textModel.getLineLength(startLineNumber) !== 0 && edit.newText.endsWith(eol) && !edit.newText.startsWith(eol)) { - return new SingleOffsetEdit(edit.replaceRange.delta(-1), eol + edit.newText.slice(0, -eol.length)); + return new StringReplacement(edit.replaceRange.delta(-1), eol + edit.newText.slice(0, -eol.length)); } return edit; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts index 80aacb461e2..413b1a4e045 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts @@ -12,11 +12,11 @@ import { SetMap } from '../../../../../base/common/map.js'; import { generateUuid } from '../../../../../base/common/uuid.js'; import { ICommandService } from '../../../../../platform/commands/common/commands.js'; import { ISingleEditOperation } from '../../../../common/core/editOperation.js'; -import { SingleOffsetEdit } from '../../../../common/core/edits/offsetEdit.js'; -import { OffsetRange } from '../../../../common/core/offsetRange.js'; +import { StringReplacement } from '../../../../common/core/edits/stringEdit.js'; +import { OffsetRange } from '../../../../common/core/ranges/offsetRange.js'; import { Position } from '../../../../common/core/position.js'; import { Range } from '../../../../common/core/range.js'; -import { SingleTextEdit } from '../../../../common/core/edits/textEdit.js'; +import { TextReplacement } from '../../../../common/core/edits/textEdit.js'; import { InlineCompletionEndOfLifeReason, InlineCompletionEndOfLifeReasonKind, InlineCompletion, InlineCompletionContext, InlineCompletionProviderGroupId, InlineCompletions, InlineCompletionsProvider, InlineCompletionTriggerKind, PartialAcceptInfo } from '../../../../common/languages.js'; import { ILanguageConfigurationService } from '../../../../common/languages/languageConfigurationRegistry.js'; import { ITextModel } from '../../../../common/model.js'; @@ -219,7 +219,7 @@ export class InlineCompletionProviderResult implements IDisposable { private readonly providerResults: readonly InlineSuggestionList[], ) { } - public has(edit: SingleTextEdit): boolean { + public has(edit: TextReplacement): boolean { return this.hashs.has(createHashFromSingleTextEdit(edit)); } @@ -236,7 +236,7 @@ export class InlineCompletionProviderResult implements IDisposable { } } -function createHashFromSingleTextEdit(edit: SingleTextEdit): string { +function createHashFromSingleTextEdit(edit: TextReplacement): string { return JSON.stringify([edit.text, edit.range.getStartPosition().toString()]); } @@ -344,7 +344,7 @@ export class InlineSuggestData { public get showInlineEditMenu() { return this.sourceInlineCompletion.showInlineEditMenu ?? false; } public getSingleTextEdit() { - return new SingleTextEdit(this.range, this.insertText); + return new TextReplacement(this.range, this.insertText); } public async reportInlineEditShown(commandService: ICommandService, updatedInsertText: string): Promise { @@ -452,10 +452,10 @@ function getDefaultRange(position: Position, model: ITextModel): Range { function closeBrackets(text: string, position: Position, model: ITextModel, languageConfigurationService: ILanguageConfigurationService): string { const currentLine = model.getLineContent(position.lineNumber); - const edit = SingleOffsetEdit.replace(new OffsetRange(position.column - 1, currentLine.length), text); + const edit = StringReplacement.replace(new OffsetRange(position.column - 1, currentLine.length), text); - const proposedLineTokens = model.tokenization.tokenizeLinesAt(position.lineNumber, [edit.apply(currentLine)]); - const textTokens = proposedLineTokens?.[0].sliceZeroCopy(edit.getRangeAfterApply()); + const proposedLineTokens = model.tokenization.tokenizeLinesAt(position.lineNumber, [edit.replace(currentLine)]); + const textTokens = proposedLineTokens?.[0].sliceZeroCopy(edit.getRangeAfterReplace()); if (!textTokens) { return text; } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/singleTextEditHelpers.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/singleTextEditHelpers.ts index 06ff9987fe2..72d9657db39 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/singleTextEditHelpers.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/singleTextEditHelpers.ts @@ -5,11 +5,11 @@ import { commonPrefixLength } from '../../../../../base/common/strings.js'; import { Range } from '../../../../common/core/range.js'; -import { TextLength } from '../../../../common/core/textLength.js'; -import { SingleTextEdit } from '../../../../common/core/edits/textEdit.js'; +import { TextLength } from '../../../../common/core/text/textLength.js'; +import { TextReplacement } from '../../../../common/core/edits/textEdit.js'; import { EndOfLinePreference, ITextModel } from '../../../../common/model.js'; -export function singleTextRemoveCommonPrefix(edit: SingleTextEdit, model: ITextModel, validModelRange?: Range): SingleTextEdit { +export function singleTextRemoveCommonPrefix(edit: TextReplacement, model: ITextModel, validModelRange?: Range): TextReplacement { const modelRange = validModelRange ? edit.range.intersectRanges(validModelRange) : edit.range; if (!modelRange) { return edit; @@ -20,10 +20,10 @@ export function singleTextRemoveCommonPrefix(edit: SingleTextEdit, model: ITextM const start = TextLength.ofText(valueToReplace.substring(0, commonPrefixLen)).addToPosition(edit.range.getStartPosition()); const text = normalizedText.substring(commonPrefixLen); const range = Range.fromPositions(start, edit.range.getEndPosition()); - return new SingleTextEdit(range, text); + return new TextReplacement(range, text); } -export function singleTextEditAugments(edit: SingleTextEdit, base: SingleTextEdit): boolean { +export function singleTextEditAugments(edit: TextReplacement, base: TextReplacement): boolean { // The augmented completion must replace the base range, but can replace even more return edit.text.startsWith(base.text) && rangeExtends(edit.range, base.range); } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/suggestWidgetAdapter.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/suggestWidgetAdapter.ts index 0c945564266..6197ac75aee 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/suggestWidgetAdapter.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/suggestWidgetAdapter.ts @@ -10,7 +10,7 @@ import { Disposable } from '../../../../../base/common/lifecycle.js'; import { ICodeEditor } from '../../../../browser/editorBrowser.js'; import { Position } from '../../../../common/core/position.js'; import { Range } from '../../../../common/core/range.js'; -import { SingleTextEdit } from '../../../../common/core/edits/textEdit.js'; +import { TextReplacement } from '../../../../common/core/edits/textEdit.js'; import { CompletionItemInsertTextRule, CompletionItemKind, SelectedSuggestionInfo } from '../../../../common/languages.js'; import { ITextModel } from '../../../../common/model.js'; import { singleTextEditAugments, singleTextRemoveCommonPrefix } from './singleTextEditHelpers.js'; @@ -34,7 +34,7 @@ export class SuggestWidgetAdaptor extends Disposable { constructor( private readonly editor: ICodeEditor, - private readonly suggestControllerPreselector: () => SingleTextEdit | undefined, + private readonly suggestControllerPreselector: () => TextReplacement | undefined, private readonly onWillAccept: (item: SuggestItemInfo) => void, ) { super(); @@ -224,8 +224,8 @@ export class SuggestItemInfo { return new SelectedSuggestionInfo(this.range, this.insertText, this.completionItemKind, this.isSnippetText); } - public getSingleTextEdit(): SingleTextEdit { - return new SingleTextEdit(this.range, this.insertText); + public getSingleTextEdit(): TextReplacement { + return new TextReplacement(this.range, this.insertText); } } @@ -240,29 +240,31 @@ function suggestItemInfoEquals(a: SuggestItemInfo | undefined, b: SuggestItemInf } export class ObservableSuggestWidgetAdapter extends Disposable { - private readonly _suggestWidgetAdaptor = this._register(new SuggestWidgetAdaptor( - this._editorObs.editor, - () => { - this._editorObs.forceUpdate(); - return this._suggestControllerPreselector(); - }, - (item) => this._editorObs.forceUpdate(_tx => { - /** @description InlineCompletionsController.handleSuggestAccepted */ - this._handleSuggestAccepted(item); - }) - )); + private readonly _suggestWidgetAdaptor; - public readonly selectedItem = observableFromEvent(this, cb => this._suggestWidgetAdaptor.onDidSelectedItemChange(() => { - this._editorObs.forceUpdate(_tx => cb(undefined)); - }), () => this._suggestWidgetAdaptor.selectedItem); + public readonly selectedItem; constructor( private readonly _editorObs: ObservableCodeEditor, private readonly _handleSuggestAccepted: (item: SuggestItemInfo) => void, - private readonly _suggestControllerPreselector: () => SingleTextEdit | undefined, + private readonly _suggestControllerPreselector: () => TextReplacement | undefined, ) { super(); + this._suggestWidgetAdaptor = this._register(new SuggestWidgetAdaptor( + this._editorObs.editor, + () => { + this._editorObs.forceUpdate(); + return this._suggestControllerPreselector(); + }, + (item) => this._editorObs.forceUpdate(_tx => { + /** @description InlineCompletionsController.handleSuggestAccepted */ + this._handleSuggestAccepted(item); + }) + )); + this.selectedItem = observableFromEvent(this, cb => this._suggestWidgetAdaptor.onDidSelectedItemChange(() => { + this._editorObs.forceUpdate(_tx => cb(undefined)); + }), () => this._suggestWidgetAdaptor.selectedItem); } public stopForceRenderingAbove(): void { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/utils.ts b/src/vs/editor/contrib/inlineCompletions/browser/utils.ts index c415242aa62..3972371ad44 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/utils.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/utils.ts @@ -10,9 +10,9 @@ import { IObservable, observableValue, ISettableObservable, autorun, transaction import { ContextKeyValue, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; import { bindContextKey } from '../../../../platform/observable/common/platformObservableUtils.js'; import { Position } from '../../../common/core/position.js'; -import { PositionOffsetTransformer } from '../../../common/core/positionToOffset.js'; +import { PositionOffsetTransformer } from '../../../common/core/text/positionToOffset.js'; import { Range } from '../../../common/core/range.js'; -import { SingleTextEdit, TextEdit } from '../../../common/core/edits/textEdit.js'; +import { TextReplacement, TextEdit } from '../../../common/core/edits/textEdit.js'; const array: ReadonlyArray = []; export function getReadonlyEmptyArray(): readonly T[] { @@ -33,12 +33,12 @@ export function substringPos(text: string, pos: Position): string { return text.substring(offset); } -export function getEndPositionsAfterApplying(edits: readonly SingleTextEdit[]): Position[] { +export function getEndPositionsAfterApplying(edits: readonly TextReplacement[]): Position[] { const newRanges = getModifiedRangesAfterApplying(edits); return newRanges.map(range => range.getEndPosition()); } -export function getModifiedRangesAfterApplying(edits: readonly SingleTextEdit[]): Range[] { +export function getModifiedRangesAfterApplying(edits: readonly TextReplacement[]): Range[] { const sortPerm = Permutation.createSortPermutation(edits, compareBy(e => e.range, Range.compareRangesUsingStarts)); const edit = new TextEdit(sortPerm.apply(edits)); const sortedNewRanges = edit.getNewRanges(); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts index 164cfe03ec1..c4d9f5b8a64 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts @@ -15,7 +15,7 @@ import { applyFontInfo } from '../../../../../browser/config/domFontInfo.js'; import { ContentWidgetPositionPreference, ICodeEditor, IContentWidgetPosition, IViewZoneChangeAccessor, MouseTargetType } from '../../../../../browser/editorBrowser.js'; import { observableCodeEditor } from '../../../../../browser/observableCodeEditor.js'; import { EditorFontLigatures, EditorOption, IComputedEditorOptions } from '../../../../../common/config/editorOptions.js'; -import { OffsetEdit, SingleOffsetEdit } from '../../../../../common/core/edits/offsetEdit.js'; +import { StringEdit, StringReplacement } from '../../../../../common/core/edits/stringEdit.js'; import { Position } from '../../../../../common/core/position.js'; import { Range } from '../../../../../common/core/range.js'; import { StringBuilder } from '../../../../../common/core/stringBuilder.js'; @@ -46,20 +46,14 @@ const USE_SQUIGGLES_FOR_WARNING = true; const GHOST_TEXT_CLASS_NAME = 'ghost-text'; export class GhostTextView extends Disposable { - private readonly _isDisposed = observableValue(this, false); - private readonly _editorObs = observableCodeEditor(this._editor); + private readonly _isDisposed; + private readonly _editorObs; public static hot = createHotClass(GhostTextView); - private _warningState = derived(reader => { - const gt = this._model.ghostText.read(reader); - if (!gt) { return undefined; } - const warning = this._model.warning.read(reader); - if (!warning) { return undefined; } - return { lineNumber: gt.lineNumber, position: new Position(gt.lineNumber, gt.parts[0].column), icon: warning.icon }; - }); + private _warningState; - private readonly _onDidClick = this._register(new Emitter()); - public readonly onDidClick = this._onDidClick.event; + private readonly _onDidClick; + public readonly onDidClick; constructor( private readonly _editor: ICodeEditor, @@ -73,6 +67,147 @@ export class GhostTextView extends Disposable { @ILanguageService private readonly _languageService: ILanguageService, ) { super(); + this._isDisposed = observableValue(this, false); + this._editorObs = observableCodeEditor(this._editor); + this._warningState = derived(reader => { + const gt = this._model.ghostText.read(reader); + if (!gt) { return undefined; } + const warning = this._model.warning.read(reader); + if (!warning) { return undefined; } + return { lineNumber: gt.lineNumber, position: new Position(gt.lineNumber, gt.parts[0].column), icon: warning.icon }; + }); + this._onDidClick = this._register(new Emitter()); + this.onDidClick = this._onDidClick.event; + this._useSyntaxHighlighting = this._options.map(o => o.syntaxHighlightingEnabled); + this._extraClassNames = derived(this, reader => { + const extraClasses = [...this._options.read(reader).extraClasses ?? []]; + if (this._useSyntaxHighlighting.read(reader)) { + extraClasses.push('syntax-highlighted'); + } + if (USE_SQUIGGLES_FOR_WARNING && this._warningState.read(reader)) { + extraClasses.push('warning'); + } + const extraClassNames = extraClasses.map(c => ` ${c}`).join(''); + return extraClassNames; + }); + this.uiState = derived(this, reader => { + if (this._isDisposed.read(reader)) { return undefined; } + const textModel = this._editorObs.model.read(reader); + if (textModel !== this._model.targetTextModel.read(reader)) { return undefined; } + const ghostText = this._model.ghostText.read(reader); + if (!ghostText) { return undefined; } + + const replacedRange = ghostText instanceof GhostTextReplacement ? ghostText.columnRange : undefined; + + const syntaxHighlightingEnabled = this._useSyntaxHighlighting.read(reader); + const extraClassNames = this._extraClassNames.read(reader); + const { inlineTexts, additionalLines, hiddenRange, additionalLinesOriginalSuffix } = computeGhostTextViewData(ghostText, textModel, GHOST_TEXT_CLASS_NAME + extraClassNames); + + const currentLine = textModel.getLineContent(ghostText.lineNumber); + const edit = new StringEdit(inlineTexts.map(t => StringReplacement.insert(t.column - 1, t.text))); + const tokens = syntaxHighlightingEnabled ? textModel.tokenization.tokenizeLinesAt(ghostText.lineNumber, [edit.apply(currentLine), ...additionalLines.map(l => l.content)]) : undefined; + const newRanges = edit.getNewRanges(); + const inlineTextsWithTokens = inlineTexts.map((t, idx) => ({ ...t, tokens: tokens?.[0]?.getTokensInRange(newRanges[idx]) })); + + const tokenizedAdditionalLines: LineData[] = additionalLines.map((l, idx) => { + let content = tokens?.[idx + 1] ?? LineTokens.createEmpty(l.content, this._languageService.languageIdCodec); + if (idx === additionalLines.length - 1 && additionalLinesOriginalSuffix) { + const t = TokenWithTextArray.fromLineTokens(textModel.tokenization.getLineTokens(additionalLinesOriginalSuffix.lineNumber)); + const existingContent = t.slice(additionalLinesOriginalSuffix.columnRange.toZeroBasedOffsetRange()); + content = TokenWithTextArray.fromLineTokens(content).append(existingContent).toLineTokens(content.languageIdCodec); + } + return { + content, + decorations: l.decorations, + }; + }); + + return { + replacedRange, + inlineTexts: inlineTextsWithTokens, + additionalLines: tokenizedAdditionalLines, + hiddenRange, + lineNumber: ghostText.lineNumber, + additionalReservedLineCount: this._model.minReservedLineCount.read(reader), + targetTextModel: textModel, + syntaxHighlightingEnabled, + }; + }); + this.decorations = derived(this, reader => { + const uiState = this.uiState.read(reader); + if (!uiState) { return []; } + + const decorations: IModelDeltaDecoration[] = []; + + const extraClassNames = this._extraClassNames.read(reader); + + if (uiState.replacedRange) { + decorations.push({ + range: uiState.replacedRange.toRange(uiState.lineNumber), + options: { inlineClassName: 'inline-completion-text-to-replace' + extraClassNames, description: 'GhostTextReplacement' } + }); + } + + if (uiState.hiddenRange) { + decorations.push({ + range: uiState.hiddenRange.toRange(uiState.lineNumber), + options: { inlineClassName: 'ghost-text-hidden', description: 'ghost-text-hidden', } + }); + } + + for (const p of uiState.inlineTexts) { + decorations.push({ + range: Range.fromPositions(new Position(uiState.lineNumber, p.column)), + options: { + description: 'ghost-text-decoration', + after: { + content: p.text, + tokens: p.tokens, + inlineClassName: (p.preview ? 'ghost-text-decoration-preview' : 'ghost-text-decoration') + + (this._isClickable ? ' clickable' : '') + + extraClassNames + + p.lineDecorations.map(d => ' ' + d.className).join(' '), // TODO: take the ranges into account for line decorations + cursorStops: InjectedTextCursorStops.Left, + attachedData: new GhostTextAttachedData(this), + }, + showIfCollapsed: true, + } + }); + } + + return decorations; + }); + this._additionalLinesWidget = this._register( + new AdditionalLinesWidget( + this._editor, + derived(reader => { + /** @description lines */ + const uiState = this.uiState.read(reader); + return uiState ? { + lineNumber: uiState.lineNumber, + additionalLines: uiState.additionalLines, + minReservedLineCount: uiState.additionalReservedLineCount, + targetTextModel: uiState.targetTextModel, + } : undefined; + }), + this._shouldKeepCursorStable, + this._isClickable + ) + ); + this._isInlineTextHovered = this._editorObs.isTargetHovered( + p => p.target.type === MouseTargetType.CONTENT_TEXT && + p.target.detail.injectedText?.options.attachedData instanceof GhostTextAttachedData && + p.target.detail.injectedText.options.attachedData.owner === this, + this._store + ); + this.isHovered = derived(this, reader => { + if (this._isDisposed.read(reader)) { return false; } + return this._isInlineTextHovered.read(reader) || this._additionalLinesWidget.isHovered.read(reader); + }); + this.height = derived(this, reader => { + const lineHeight = this._editorObs.getOption(EditorOption.lineHeight).read(reader); + return lineHeight + (this._additionalLinesWidget.viewZoneHeight.read(reader) ?? 0); + }); this._register(toDisposable(() => { this._isDisposed.set(true, undefined); })); this._register(this._editorObs.setDecorations(this.decorations)); @@ -147,143 +282,21 @@ export class GhostTextView extends Disposable { return undefined; } - private readonly _useSyntaxHighlighting = this._options.map(o => o.syntaxHighlightingEnabled); + private readonly _useSyntaxHighlighting; - private readonly _extraClassNames = derived(this, reader => { - const extraClasses = [...this._options.read(reader).extraClasses ?? []]; - if (this._useSyntaxHighlighting.read(reader)) { - extraClasses.push('syntax-highlighted'); - } - if (USE_SQUIGGLES_FOR_WARNING && this._warningState.read(reader)) { - extraClasses.push('warning'); - } - const extraClassNames = extraClasses.map(c => ` ${c}`).join(''); - return extraClassNames; - }); + private readonly _extraClassNames; - private readonly uiState = derived(this, reader => { - if (this._isDisposed.read(reader)) { return undefined; } - const textModel = this._editorObs.model.read(reader); - if (textModel !== this._model.targetTextModel.read(reader)) { return undefined; } - const ghostText = this._model.ghostText.read(reader); - if (!ghostText) { return undefined; } + private readonly uiState; - const replacedRange = ghostText instanceof GhostTextReplacement ? ghostText.columnRange : undefined; + private readonly decorations; - const syntaxHighlightingEnabled = this._useSyntaxHighlighting.read(reader); - const extraClassNames = this._extraClassNames.read(reader); - const { inlineTexts, additionalLines, hiddenRange, additionalLinesOriginalSuffix } = computeGhostTextViewData(ghostText, textModel, GHOST_TEXT_CLASS_NAME + extraClassNames); + private readonly _additionalLinesWidget; - const currentLine = textModel.getLineContent(ghostText.lineNumber); - const edit = new OffsetEdit(inlineTexts.map(t => SingleOffsetEdit.insert(t.column - 1, t.text))); - const tokens = syntaxHighlightingEnabled ? textModel.tokenization.tokenizeLinesAt(ghostText.lineNumber, [edit.apply(currentLine), ...additionalLines.map(l => l.content)]) : undefined; - const newRanges = edit.getNewTextRanges(); - const inlineTextsWithTokens = inlineTexts.map((t, idx) => ({ ...t, tokens: tokens?.[0]?.getTokensInRange(newRanges[idx]) })); + private readonly _isInlineTextHovered; - const tokenizedAdditionalLines: LineData[] = additionalLines.map((l, idx) => { - let content = tokens?.[idx + 1] ?? LineTokens.createEmpty(l.content, this._languageService.languageIdCodec); - if (idx === additionalLines.length - 1 && additionalLinesOriginalSuffix) { - const t = TokenWithTextArray.fromLineTokens(textModel.tokenization.getLineTokens(additionalLinesOriginalSuffix.lineNumber)); - const existingContent = t.slice(additionalLinesOriginalSuffix.columnRange.toZeroBasedOffsetRange()); - content = TokenWithTextArray.fromLineTokens(content).append(existingContent).toLineTokens(content.languageIdCodec); - } - return { - content, - decorations: l.decorations, - }; - }); + public readonly isHovered; - return { - replacedRange, - inlineTexts: inlineTextsWithTokens, - additionalLines: tokenizedAdditionalLines, - hiddenRange, - lineNumber: ghostText.lineNumber, - additionalReservedLineCount: this._model.minReservedLineCount.read(reader), - targetTextModel: textModel, - syntaxHighlightingEnabled, - }; - }); - - private readonly decorations = derived(this, reader => { - const uiState = this.uiState.read(reader); - if (!uiState) { return []; } - - const decorations: IModelDeltaDecoration[] = []; - - const extraClassNames = this._extraClassNames.read(reader); - - if (uiState.replacedRange) { - decorations.push({ - range: uiState.replacedRange.toRange(uiState.lineNumber), - options: { inlineClassName: 'inline-completion-text-to-replace' + extraClassNames, description: 'GhostTextReplacement' } - }); - } - - if (uiState.hiddenRange) { - decorations.push({ - range: uiState.hiddenRange.toRange(uiState.lineNumber), - options: { inlineClassName: 'ghost-text-hidden', description: 'ghost-text-hidden', } - }); - } - - for (const p of uiState.inlineTexts) { - decorations.push({ - range: Range.fromPositions(new Position(uiState.lineNumber, p.column)), - options: { - description: 'ghost-text-decoration', - after: { - content: p.text, - tokens: p.tokens, - inlineClassName: (p.preview ? 'ghost-text-decoration-preview' : 'ghost-text-decoration') - + (this._isClickable ? ' clickable' : '') - + extraClassNames - + p.lineDecorations.map(d => ' ' + d.className).join(' '), // TODO: take the ranges into account for line decorations - cursorStops: InjectedTextCursorStops.Left, - attachedData: new GhostTextAttachedData(this), - }, - showIfCollapsed: true, - } - }); - } - - return decorations; - }); - - private readonly _additionalLinesWidget = this._register( - new AdditionalLinesWidget( - this._editor, - derived(reader => { - /** @description lines */ - const uiState = this.uiState.read(reader); - return uiState ? { - lineNumber: uiState.lineNumber, - additionalLines: uiState.additionalLines, - minReservedLineCount: uiState.additionalReservedLineCount, - targetTextModel: uiState.targetTextModel, - } : undefined; - }), - this._shouldKeepCursorStable, - this._isClickable - ) - ); - - private readonly _isInlineTextHovered = this._editorObs.isTargetHovered( - p => p.target.type === MouseTargetType.CONTENT_TEXT && - p.target.detail.injectedText?.options.attachedData instanceof GhostTextAttachedData && - p.target.detail.injectedText.options.attachedData.owner === this, - this._store - ); - - public readonly isHovered = derived(this, reader => { - if (this._isDisposed.read(reader)) { return false; } - return this._isInlineTextHovered.read(reader) || this._additionalLinesWidget.isHovered.read(reader); - }); - - public readonly height = derived(this, reader => { - const lineHeight = this._editorObs.getOption(EditorOption.lineHeight).read(reader); - return lineHeight + (this._additionalLinesWidget.viewZoneHeight.read(reader) ?? 0); - }); + public readonly height; public ownsViewZone(viewZoneId: string): boolean { return this._additionalLinesWidget.viewZoneId === viewZoneId; @@ -373,31 +386,19 @@ export class AdditionalLinesWidget extends Disposable { private _viewZoneInfo: { viewZoneId: string; heightInLines: number; lineNumber: number } | undefined; public get viewZoneId(): string | undefined { return this._viewZoneInfo?.viewZoneId; } - private _viewZoneHeight = observableValue('viewZoneHeight', undefined); + private _viewZoneHeight; public get viewZoneHeight(): IObservable { return this._viewZoneHeight; } - private readonly editorOptionsChanged = observableSignalFromEvent('editorOptionChanged', Event.filter( - this._editor.onDidChangeConfiguration, - e => e.hasChanged(EditorOption.disableMonospaceOptimizations) - || e.hasChanged(EditorOption.stopRenderingLineAfter) - || e.hasChanged(EditorOption.renderWhitespace) - || e.hasChanged(EditorOption.renderControlCharacters) - || e.hasChanged(EditorOption.fontLigatures) - || e.hasChanged(EditorOption.fontInfo) - || e.hasChanged(EditorOption.lineHeight) - )); + private readonly editorOptionsChanged; - private readonly _onDidClick = this._register(new Emitter()); - public readonly onDidClick = this._onDidClick.event; + private readonly _onDidClick; + public readonly onDidClick; - private readonly _viewZoneListener = this._register(new MutableDisposable()); + private readonly _viewZoneListener; - readonly isHovered = observableCodeEditor(this._editor).isTargetHovered( - p => isTargetGhostText(p.target.element), - this._store - ); + readonly isHovered; - private hasBeenAccepted = false; + private hasBeenAccepted; constructor( private readonly _editor: ICodeEditor, @@ -411,6 +412,25 @@ export class AdditionalLinesWidget extends Disposable { private readonly _isClickable: boolean, ) { super(); + this._viewZoneHeight = observableValue('viewZoneHeight', undefined); + this.editorOptionsChanged = observableSignalFromEvent('editorOptionChanged', Event.filter( + this._editor.onDidChangeConfiguration, + e => e.hasChanged(EditorOption.disableMonospaceOptimizations) + || e.hasChanged(EditorOption.stopRenderingLineAfter) + || e.hasChanged(EditorOption.renderWhitespace) + || e.hasChanged(EditorOption.renderControlCharacters) + || e.hasChanged(EditorOption.fontLigatures) + || e.hasChanged(EditorOption.fontInfo) + || e.hasChanged(EditorOption.lineHeight) + )); + this._onDidClick = this._register(new Emitter()); + this.onDidClick = this._onDidClick.event; + this._viewZoneListener = this._register(new MutableDisposable()); + this.isHovered = observableCodeEditor(this._editor).isTargetHovered( + p => isTargetGhostText(p.target.element), + this._store + ); + this.hasBeenAccepted = false; if (this._editor instanceof CodeEditorWidget && this._shouldKeepCursorStable) { this._register(this._editor.onBeforeExecuteEdit(e => this.hasBeenAccepted = e.source === 'inlineSuggestion.accept')); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineCompletionsView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineCompletionsView.ts index 7437ac73eb9..0aca2fe89e5 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineCompletionsView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineCompletionsView.ts @@ -17,44 +17,18 @@ import { GhostTextView } from './ghostText/ghostTextView.js'; import { InlineEditsViewAndDiffProducer } from './inlineEdits/inlineEditsViewProducer.js'; export class InlineCompletionsView extends Disposable { - private readonly _ghostTexts = derived(this, (reader) => { - const model = this._model.read(reader); - return model?.ghostTexts.read(reader) ?? []; - }); + private readonly _ghostTexts; - private readonly _stablizedGhostTexts = convertItemsToStableObservables(this._ghostTexts, this._store); - private readonly _editorObs = observableCodeEditor(this._editor); + private readonly _stablizedGhostTexts; + private readonly _editorObs; - private readonly _ghostTextWidgets = mapObservableArrayCached(this, this._stablizedGhostTexts, (ghostText, store) => derivedDisposable((reader) => this._instantiationService.createInstance( - GhostTextView.hot.read(reader), - this._editor, - { - ghostText: ghostText, - warning: this._model.map((m, reader) => { - const warning = m?.warning?.read(reader); - return warning ? { icon: warning.icon } : undefined; - }), - minReservedLineCount: constObservable(0), - targetTextModel: this._model.map(v => v?.textModel), - }, - this._editorObs.getOption(EditorOption.inlineSuggest).map(v => ({ syntaxHighlightingEnabled: v.syntaxHighlightingEnabled })), - false, - false - ) - ).recomputeInitiallyAndOnChange(store) - ).recomputeInitiallyAndOnChange(this._store); + private readonly _ghostTextWidgets; - private readonly _inlineEdit = derived(this, reader => this._model.read(reader)?.inlineEditState.read(reader)?.inlineEdit); - private readonly _everHadInlineEdit = derivedObservableWithCache(this, (reader, last) => last || !!this._inlineEdit.read(reader) || !!this._model.read(reader)?.inlineCompletionState.read(reader)?.inlineCompletion?.showInlineEditMenu); - protected readonly _inlineEditWidget = derivedDisposable(reader => { - if (!this._everHadInlineEdit.read(reader)) { - return undefined; - } - return this._instantiationService.createInstance(InlineEditsViewAndDiffProducer.hot.read(reader), this._editor, this._inlineEdit, this._model, this._focusIsInMenu); - }) - .recomputeInitiallyAndOnChange(this._store); + private readonly _inlineEdit; + private readonly _everHadInlineEdit; + protected readonly _inlineEditWidget; - private readonly _fontFamily = this._editorObs.getOption(EditorOption.inlineSuggest).map(val => val.fontFamily); + private readonly _fontFamily; constructor( private readonly _editor: ICodeEditor, @@ -63,6 +37,40 @@ export class InlineCompletionsView extends Disposable { @IInstantiationService private readonly _instantiationService: IInstantiationService, ) { super(); + this._ghostTexts = derived(this, (reader) => { + const model = this._model.read(reader); + return model?.ghostTexts.read(reader) ?? []; + }); + this._stablizedGhostTexts = convertItemsToStableObservables(this._ghostTexts, this._store); + this._editorObs = observableCodeEditor(this._editor); + this._ghostTextWidgets = mapObservableArrayCached(this, this._stablizedGhostTexts, (ghostText, store) => derivedDisposable((reader) => this._instantiationService.createInstance( + GhostTextView.hot.read(reader), + this._editor, + { + ghostText: ghostText, + warning: this._model.map((m, reader) => { + const warning = m?.warning?.read(reader); + return warning ? { icon: warning.icon } : undefined; + }), + minReservedLineCount: constObservable(0), + targetTextModel: this._model.map(v => v?.textModel), + }, + this._editorObs.getOption(EditorOption.inlineSuggest).map(v => ({ syntaxHighlightingEnabled: v.syntaxHighlightingEnabled })), + false, + false + ) + ).recomputeInitiallyAndOnChange(store) + ).recomputeInitiallyAndOnChange(this._store); + this._inlineEdit = derived(this, reader => this._model.read(reader)?.inlineEditState.read(reader)?.inlineEdit); + this._everHadInlineEdit = derivedObservableWithCache(this, (reader, last) => last || !!this._inlineEdit.read(reader) || !!this._model.read(reader)?.inlineCompletionState.read(reader)?.inlineCompletion?.showInlineEditMenu); + this._inlineEditWidget = derivedDisposable(reader => { + if (!this._everHadInlineEdit.read(reader)) { + return undefined; + } + return this._instantiationService.createInstance(InlineEditsViewAndDiffProducer.hot.read(reader), this._editor, this._inlineEdit, this._model, this._focusIsInMenu); + }) + .recomputeInitiallyAndOnChange(this._store); + this._fontFamily = this._editorObs.getOption(EditorOption.inlineSuggest).map(val => val.fontFamily); this._register(createStyleSheetFromObservable(derived(reader => { const fontFamily = this._fontFamily.read(reader); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorMenu.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorMenu.ts index 57d3134c250..7af3360936e 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorMenu.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorMenu.ts @@ -10,7 +10,7 @@ import { KeybindingLabel } from '../../../../../../../base/browser/ui/keybinding import { IAction } from '../../../../../../../base/common/actions.js'; import { Codicon } from '../../../../../../../base/common/codicons.js'; import { ResolvedKeybinding } from '../../../../../../../base/common/keybindings.js'; -import { IObservable, autorun, constObservable, derived, derivedWithStore, observableFromEvent, observableValue } from '../../../../../../../base/common/observable.js'; +import { IObservable, autorun, constObservable, derived, observableFromEvent, observableValue } from '../../../../../../../base/common/observable.js'; import { OS } from '../../../../../../../base/common/platform.js'; import { ThemeIcon } from '../../../../../../../base/common/themables.js'; import { localize } from '../../../../../../../nls.js'; @@ -22,8 +22,8 @@ import { defaultKeybindingLabelStyles } from '../../../../../../../platform/them import { asCssVariable, descriptionForeground, editorActionListForeground, editorHoverBorder, keybindingLabelBackground } from '../../../../../../../platform/theme/common/colorRegistry.js'; import { ObservableCodeEditor } from '../../../../../../browser/observableCodeEditor.js'; import { EditorOption } from '../../../../../../common/config/editorOptions.js'; -import { hideInlineCompletionId, inlineSuggestCommitId, jumpToNextInlineEditId, toggleShowCollapsedId } from '../../../controller/commandIds.js'; -import { IInlineEditModel, InlineEditTabAction } from '../inlineEditsViewInterface.js'; +import { hideInlineCompletionId, inlineSuggestCommitId, toggleShowCollapsedId } from '../../../controller/commandIds.js'; +import { IInlineEditModel } from '../inlineEditsViewInterface.js'; import { FirstFnArg, } from '../utils/utils.js'; export class GutterIndicatorMenuContent { @@ -67,8 +67,8 @@ export class GutterIndicatorMenuContent { const gotoAndAccept = option(createOptionArgs({ id: 'gotoAndAccept', title: `${localize('goto', "Go To")} / ${localize('accept', "Accept")}`, - icon: this._model.tabAction.map(action => action === InlineEditTabAction.Accept ? Codicon.check : Codicon.arrowRight), - commandId: this._model.tabAction.map(action => action === InlineEditTabAction.Accept ? inlineSuggestCommitId : jumpToNextInlineEditId) + icon: Codicon.check, + commandId: inlineSuggestCommitId })); const reject = option(createOptionArgs({ @@ -170,7 +170,7 @@ function option(props: { onHoverChange?: (isHovered: boolean) => void; onAction?: () => void; }) { - return derivedWithStore((_reader, store) => n.div({ + return derived((_reader) => n.div({ class: ['monaco-menu-option', props.isActive?.map(v => v && 'active')], onmouseenter: () => props.onHoverChange?.(true), onmouseleave: () => props.onHoverChange?.(false), @@ -195,7 +195,7 @@ function option(props: { n.div({ style: { marginLeft: 'auto' }, ref: elem => { - const keybindingLabel = store.add(new KeybindingLabel(elem, OS, { + const keybindingLabel = _reader.store.add(new KeybindingLabel(elem, OS, { disableTitle: true, ...defaultKeybindingLabelStyles, keybindingLabelShadow: undefined, @@ -203,7 +203,7 @@ function option(props: { keybindingLabelBorder: 'transparent', keybindingLabelBottomBorder: undefined, })); - store.add(autorun(reader => { + _reader.store.add(autorun(reader => { keybindingLabel.set(props.keybinding.read(reader)); })); } @@ -213,7 +213,7 @@ function option(props: { // TODO: make this observable function actionBar(actions: IAction[], options: IActionBarOptions) { - return derivedWithStore((_reader, store) => n.div({ + return derived((_reader) => n.div({ class: ['action-widget-action-bar'], style: { padding: '0 10px', @@ -221,7 +221,7 @@ function actionBar(actions: IAction[], options: IActionBarOptions) { }, [ n.div({ ref: elem => { - const actionBar = store.add(new ActionBar(elem, options)); + const actionBar = _reader.store.add(new ActionBar(elem, options)); actionBar.push(actions, { icon: false, label: true }); } }) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorView.ts index 651914fa457..d8c7bb28da7 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorView.ts @@ -22,7 +22,7 @@ import { HoverService } from '../../../../../../browser/services/hoverService/ho import { HoverWidget } from '../../../../../../browser/services/hoverService/hoverWidget.js'; import { EditorOption, RenderLineNumbersType } from '../../../../../../common/config/editorOptions.js'; import { LineRange } from '../../../../../../common/core/ranges/lineRange.js'; -import { OffsetRange } from '../../../../../../common/core/offsetRange.js'; +import { OffsetRange } from '../../../../../../common/core/ranges/offsetRange.js'; import { StickyScrollController } from '../../../../../stickyScroll/browser/stickyScrollController.js'; import { IInlineEditModel, InlineEditTabAction } from '../inlineEditsViewInterface.js'; import { getEditorBlendedColor, inlineEditIndicatorBackground, inlineEditIndicatorPrimaryBackground, inlineEditIndicatorPrimaryBorder, inlineEditIndicatorPrimaryForeground, inlineEditIndicatorSecondaryBackground, inlineEditIndicatorSecondaryBorder, inlineEditIndicatorSecondaryForeground, inlineEditIndicatorsuccessfulBackground, inlineEditIndicatorsuccessfulBorder, inlineEditIndicatorsuccessfulForeground } from '../theme.js'; @@ -54,6 +54,12 @@ export class InlineEditsGutterIndicator extends Disposable { ) { super(); + this._tabAction = derived(this, reader => { + const model = this._model.read(reader); + if (!model) { return InlineEditTabAction.Inactive; } + return model.tabAction.read(reader); + }); + this._gutterIndicatorStyles = this._tabAction.map((v, reader) => { switch (v) { case InlineEditTabAction.Inactive: return { @@ -74,6 +80,298 @@ export class InlineEditsGutterIndicator extends Disposable { } }); + this._originalRangeObs = mapOutFalsy(this._originalRange); + this._state = derived(reader => { + const range = this._originalRangeObs.read(reader); + if (!range) { return undefined; } + return { + range, + lineOffsetRange: this._editorObs.observeLineOffsetRange(range, this._store), + }; + }); + this._stickyScrollController = StickyScrollController.get(this._editorObs.editor); + this._stickyScrollHeight = this._stickyScrollController + ? observableFromEvent(this._stickyScrollController.onDidChangeStickyScrollHeight, () => this._stickyScrollController!.stickyScrollWidgetHeight) + : constObservable(0); + this._lineNumberToRender = derived(this, reader => { + if (this._verticalOffset.read(reader) !== 0) { + return ''; + } + + const lineNumber = this._originalRange.read(reader)?.startLineNumber; + const lineNumberOptions = this._editorObs.getOption(EditorOption.lineNumbers).read(reader); + + if (lineNumber === undefined || lineNumberOptions.renderType === RenderLineNumbersType.Off) { + return ''; + } + + if (lineNumberOptions.renderType === RenderLineNumbersType.Interval) { + const cursorPosition = this._editorObs.cursorPosition.read(reader); + if (lineNumber % 10 === 0 || cursorPosition && cursorPosition.lineNumber === lineNumber) { + return lineNumber.toString(); + } + return ''; + } + + if (lineNumberOptions.renderType === RenderLineNumbersType.Relative) { + const cursorPosition = this._editorObs.cursorPosition.read(reader); + if (!cursorPosition) { + return ''; + } + const relativeLineNumber = Math.abs(lineNumber - cursorPosition.lineNumber); + if (relativeLineNumber === 0) { + return lineNumber.toString(); + } + return relativeLineNumber.toString(); + } + + if (lineNumberOptions.renderType === RenderLineNumbersType.Custom) { + if (lineNumberOptions.renderFn) { + return lineNumberOptions.renderFn(lineNumber); + } + return ''; + } + + return lineNumber.toString(); + }); + this._availableWidthForIcon = derived(this, reader => { + const textModel = this._editorObs.editor.getModel(); + const editor = this._editorObs.editor; + const layout = this._editorObs.layoutInfo.read(reader); + const gutterWidth = layout.decorationsLeft + layout.decorationsWidth - layout.glyphMarginLeft; + + if (!textModel || gutterWidth <= 0) { + return () => 0; + } + + // no glyph margin => the entire gutter width is available as there is no optimal place to put the icon + if (layout.lineNumbersLeft === 0) { + return () => gutterWidth; + } + + const lineNumberOptions = this._editorObs.getOption(EditorOption.lineNumbers).read(reader); + if (lineNumberOptions.renderType === RenderLineNumbersType.Relative || /* likely to flicker */ + lineNumberOptions.renderType === RenderLineNumbersType.Off) { + return () => gutterWidth; + } + + const w = editor.getOption(EditorOption.fontInfo).typicalHalfwidthCharacterWidth; + const rightOfLineNumber = layout.lineNumbersLeft + layout.lineNumbersWidth; + const totalLines = textModel.getLineCount(); + const totalLinesDigits = (totalLines + 1 /* 0 based to 1 based*/).toString().length; + + const offsetDigits: { + firstLineNumberWithDigitCount: number; + topOfLineNumber: number; + usableWidthLeftOfLineNumber: number; + }[] = []; + + // We only need to pre compute the usable width left of the line number for the first line number with a given digit count + for (let digits = 1; digits <= totalLinesDigits; digits++) { + const firstLineNumberWithDigitCount = 10 ** (digits - 1); + const topOfLineNumber = editor.getTopForLineNumber(firstLineNumberWithDigitCount); + const digitsWidth = digits * w; + const usableWidthLeftOfLineNumber = Math.min(gutterWidth, Math.max(0, rightOfLineNumber - digitsWidth - layout.glyphMarginLeft)); + offsetDigits.push({ firstLineNumberWithDigitCount, topOfLineNumber, usableWidthLeftOfLineNumber }); + } + + return (topOffset: number) => { + for (let i = offsetDigits.length - 1; i >= 0; i--) { + if (topOffset >= offsetDigits[i].topOfLineNumber) { + return offsetDigits[i].usableWidthLeftOfLineNumber; + } + } + throw new BugIndicatingError('Could not find avilable width for icon'); + }; + }); + this._layout = derived(this, reader => { + const s = this._state.read(reader); + if (!s) { return undefined; } + + const layout = this._editorObs.layoutInfo.read(reader); + + const lineHeight = this._editorObs.getOption(EditorOption.lineHeight).read(reader); + const gutterViewPortPadding = 1; + + // Entire gutter view from top left to bottom right + const gutterWidthWithoutPadding = layout.decorationsLeft + layout.decorationsWidth - layout.glyphMarginLeft - 2 * gutterViewPortPadding; + const gutterHeightWithoutPadding = layout.height - 2 * gutterViewPortPadding; + const gutterViewPortWithStickyScroll = Rect.fromLeftTopWidthHeight(gutterViewPortPadding, gutterViewPortPadding, gutterWidthWithoutPadding, gutterHeightWithoutPadding); + const gutterViewPortWithoutStickyScrollWithoutPaddingTop = gutterViewPortWithStickyScroll.withTop(this._stickyScrollHeight.read(reader)); + const gutterViewPortWithoutStickyScroll = gutterViewPortWithStickyScroll.withTop(gutterViewPortWithoutStickyScrollWithoutPaddingTop.top + gutterViewPortPadding); + + // The glyph margin area across all relevant lines + const verticalEditRange = s.lineOffsetRange.read(reader); + const gutterEditArea = Rect.fromRanges(OffsetRange.fromTo(gutterViewPortWithoutStickyScroll.left, gutterViewPortWithoutStickyScroll.right), verticalEditRange); + + // The gutter view container (pill) + const pillHeight = lineHeight; + const pillOffset = this._verticalOffset.read(reader); + const pillFullyDockedRect = gutterEditArea.withHeight(pillHeight).translateY(pillOffset); + const pillIsFullyDocked = gutterViewPortWithoutStickyScrollWithoutPaddingTop.containsRect(pillFullyDockedRect); + + // The icon which will be rendered in the pill + const iconNoneDocked = this._tabAction.map(action => action === InlineEditTabAction.Accept ? Codicon.keyboardTab : Codicon.arrowRight); + const iconDocked = derived(reader => { + if (this._isHoveredOverIconDebounced.read(reader) || this._isHoveredOverInlineEditDebounced.read(reader)) { + return Codicon.check; + } + if (this._tabAction.read(reader) === InlineEditTabAction.Accept) { + return Codicon.keyboardTab; + } + const cursorLineNumber = this._editorObs.cursorLineNumber.read(reader) ?? 0; + const editStartLineNumber = s.range.read(reader).startLineNumber; + return cursorLineNumber <= editStartLineNumber ? Codicon.keyboardTabAbove : Codicon.keyboardTabBelow; + }); + + const idealIconWidth = 22; + const minimalIconWidth = 16; // codicon size + const iconWidth = (pillRect: Rect) => { + const availableWidth = this._availableWidthForIcon.get()(pillRect.bottom + this._editorObs.editor.getScrollTop()) - gutterViewPortPadding; + return Math.max(Math.min(availableWidth, idealIconWidth), minimalIconWidth); + }; + + if (pillIsFullyDocked) { + const pillRect = pillFullyDockedRect; + const lineNumberWidth = Math.max(layout.lineNumbersLeft + layout.lineNumbersWidth - gutterViewPortWithStickyScroll.left, 0); + const lineNumberRect = pillRect.withWidth(lineNumberWidth); + const iconWidth = Math.max(Math.min(layout.decorationsWidth, idealIconWidth), minimalIconWidth); + const iconRect = pillRect.withWidth(iconWidth).translateX(lineNumberWidth); + + return { + gutterEditArea, + icon: iconDocked, + iconDirection: 'right' as const, + iconRect, + pillRect, + lineNumberRect, + }; + } + + const pillPartiallyDockedPossibleArea = gutterViewPortWithStickyScroll.intersect(gutterEditArea); // The area in which the pill could be partially docked + const pillIsPartiallyDocked = pillPartiallyDockedPossibleArea && pillPartiallyDockedPossibleArea.height >= pillHeight; + + if (pillIsPartiallyDocked) { + // pillFullyDockedRect is outside viewport, move it into the viewport under sticky scroll as we prefer the pill to not be on top of the sticky scroll + // then move it into the possible area which will only cause it to move if it has to be rendered on top of the sticky scroll + const pillRectMoved = pillFullyDockedRect.moveToBeContainedIn(gutterViewPortWithoutStickyScroll).moveToBeContainedIn(pillPartiallyDockedPossibleArea); + const pillRect = pillRectMoved.withWidth(iconWidth(pillRectMoved)); + const iconRect = pillRect; + + return { + gutterEditArea, + icon: iconDocked, + iconDirection: 'right' as const, + iconRect, + pillRect, + }; + } + + // pillFullyDockedRect is outside viewport, so move it into viewport + const pillRectMoved = pillFullyDockedRect.moveToBeContainedIn(gutterViewPortWithStickyScroll); + const pillRect = pillRectMoved.withWidth(iconWidth(pillRectMoved)); + const iconRect = pillRect; + + // docked = pill was already in the viewport + const iconDirection = pillRect.top < pillFullyDockedRect.top ? + 'top' as const : + 'bottom' as const; + + return { + gutterEditArea, + icon: iconNoneDocked, + iconDirection, + iconRect, + pillRect, + }; + }); + this._iconRef = n.ref(); + this.isVisible = this._layout.map(l => !!l); + this._hoverVisible = observableValue(this, false); + this.isHoverVisible = this._hoverVisible; + this._isHoveredOverIcon = observableValue(this, false); + this._isHoveredOverIconDebounced = debouncedObservable(this._isHoveredOverIcon, 100); + this.isHoveredOverIcon = this._isHoveredOverIconDebounced; + this._indicator = n.div({ + class: 'inline-edits-view-gutter-indicator', + onclick: () => { + const layout = this._layout.get(); + const acceptOnClick = layout?.icon.get() === Codicon.check; + + this._editorObs.editor.focus(); + if (acceptOnClick) { + this.model.accept(); + } else { + this.model.jump(); + } + }, + tabIndex: 0, + style: { + position: 'absolute', + overflow: 'visible', + }, + }, mapOutFalsy(this._layout).map(layout => !layout ? [] : [ + n.div({ + style: { + position: 'absolute', + background: asCssVariable(inlineEditIndicatorBackground), + borderRadius: '4px', + ...rectToProps(reader => layout.read(reader).gutterEditArea), + } + }), + n.div({ + class: 'icon', + ref: this._iconRef, + onmouseenter: () => { + // TODO show hover when hovering ghost text etc. + this._showHover(); + }, + style: { + cursor: 'pointer', + zIndex: '20', + position: 'absolute', + backgroundColor: this._gutterIndicatorStyles.map(v => v.background), + ['--vscodeIconForeground' as any]: this._gutterIndicatorStyles.map(v => v.foreground), + border: this._gutterIndicatorStyles.map(v => `1px solid ${v.border}`), + boxSizing: 'border-box', + borderRadius: '4px', + display: 'flex', + justifyContent: 'flex-end', + transition: 'background-color 0.2s ease-in-out, width 0.2s ease-in-out', + ...rectToProps(reader => layout.read(reader).pillRect), + } + }, [ + n.div({ + className: 'line-number', + style: { + lineHeight: layout.map(l => l.lineNumberRect ? l.lineNumberRect.height : 0), + display: layout.map(l => l.lineNumberRect ? 'flex' : 'none'), + alignItems: 'center', + justifyContent: 'flex-end', + width: layout.map(l => l.lineNumberRect ? l.lineNumberRect.width : 0), + height: '100%', + color: this._gutterIndicatorStyles.map(v => v.foreground), + } + }, + this._lineNumberToRender + ), + n.div({ + style: { + rotate: layout.map(l => `${getRotationFromDirection(l.iconDirection)}deg`), + transition: 'rotate 0.2s ease-in-out', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + height: '100%', + marginRight: layout.map(l => l.pillRect.width - l.iconRect.width - (l.lineNumberRect?.width ?? 0)), + width: layout.map(l => l.iconRect.width), + } + }, [ + layout.map((l, reader) => renderIcon(l.icon.read(reader))), + ]) + ]), + ])).keepUpdated(this._store); + this._register(this._editorObs.createOverlayWidget({ domNode: this._indicator.element, position: constObservable(null), @@ -135,227 +433,30 @@ export class InlineEditsGutterIndicator extends Disposable { return animation.finished; } - private readonly _originalRangeObs = mapOutFalsy(this._originalRange); + private readonly _originalRangeObs; - private readonly _state = derived(reader => { - const range = this._originalRangeObs.read(reader); - if (!range) { return undefined; } - return { - range, - lineOffsetRange: this._editorObs.observeLineOffsetRange(range, this._store), - }; - }); + private readonly _state; - private readonly _stickyScrollController = StickyScrollController.get(this._editorObs.editor); - private readonly _stickyScrollHeight = this._stickyScrollController - ? observableFromEvent(this._stickyScrollController.onDidChangeStickyScrollHeight, () => this._stickyScrollController!.stickyScrollWidgetHeight) - : constObservable(0); + private readonly _stickyScrollController; + private readonly _stickyScrollHeight; - private readonly _lineNumberToRender = derived(this, reader => { - if (this._verticalOffset.read(reader) !== 0) { - return ''; - } + private readonly _lineNumberToRender; - const lineNumber = this._originalRange.read(reader)?.startLineNumber; - const lineNumberOptions = this._editorObs.getOption(EditorOption.lineNumbers).read(reader); + private readonly _availableWidthForIcon; - if (lineNumber === undefined || lineNumberOptions.renderType === RenderLineNumbersType.Off) { - return ''; - } - - if (lineNumberOptions.renderType === RenderLineNumbersType.Interval) { - const cursorPosition = this._editorObs.cursorPosition.read(reader); - if (lineNumber % 10 === 0 || cursorPosition && cursorPosition.lineNumber === lineNumber) { - return lineNumber.toString(); - } - return ''; - } - - if (lineNumberOptions.renderType === RenderLineNumbersType.Relative) { - const cursorPosition = this._editorObs.cursorPosition.read(reader); - if (!cursorPosition) { - return ''; - } - const relativeLineNumber = Math.abs(lineNumber - cursorPosition.lineNumber); - if (relativeLineNumber === 0) { - return lineNumber.toString(); - } - return relativeLineNumber.toString(); - } - - if (lineNumberOptions.renderType === RenderLineNumbersType.Custom) { - if (lineNumberOptions.renderFn) { - return lineNumberOptions.renderFn(lineNumber); - } - return ''; - } - - return lineNumber.toString(); - }); - - private readonly _availableWidthForIcon = derived(this, reader => { - const textModel = this._editorObs.editor.getModel(); - const editor = this._editorObs.editor; - const layout = this._editorObs.layoutInfo.read(reader); - const gutterWidth = layout.decorationsLeft + layout.decorationsWidth - layout.glyphMarginLeft; - - if (!textModel || gutterWidth <= 0) { - return () => 0; - } - - // no glyph margin => the entire gutter width is available as there is no optimal place to put the icon - if (layout.lineNumbersLeft === 0) { - return () => gutterWidth; - } - - const lineNumberOptions = this._editorObs.getOption(EditorOption.lineNumbers).read(reader); - if (lineNumberOptions.renderType === RenderLineNumbersType.Relative || /* likely to flicker */ - lineNumberOptions.renderType === RenderLineNumbersType.Off) { - return () => gutterWidth; - } - - const w = editor.getOption(EditorOption.fontInfo).typicalHalfwidthCharacterWidth; - const rightOfLineNumber = layout.lineNumbersLeft + layout.lineNumbersWidth; - const totalLines = textModel.getLineCount(); - const totalLinesDigits = (totalLines + 1 /* 0 based to 1 based*/).toString().length; - - const offsetDigits: { - firstLineNumberWithDigitCount: number; - topOfLineNumber: number; - usableWidthLeftOfLineNumber: number; - }[] = []; - - // We only need to pre compute the usable width left of the line number for the first line number with a given digit count - for (let digits = 1; digits <= totalLinesDigits; digits++) { - const firstLineNumberWithDigitCount = 10 ** (digits - 1); - const topOfLineNumber = editor.getTopForLineNumber(firstLineNumberWithDigitCount); - const digitsWidth = digits * w; - const usableWidthLeftOfLineNumber = Math.min(gutterWidth, Math.max(0, rightOfLineNumber - digitsWidth - layout.glyphMarginLeft)); - offsetDigits.push({ firstLineNumberWithDigitCount, topOfLineNumber, usableWidthLeftOfLineNumber }); - } - - return (topOffset: number) => { - for (let i = offsetDigits.length - 1; i >= 0; i--) { - if (topOffset >= offsetDigits[i].topOfLineNumber) { - return offsetDigits[i].usableWidthLeftOfLineNumber; - } - } - throw new BugIndicatingError('Could not find avilable width for icon'); - }; - }); - - private readonly _layout = derived(this, reader => { - const s = this._state.read(reader); - if (!s) { return undefined; } - - const layout = this._editorObs.layoutInfo.read(reader); - - const lineHeight = this._editorObs.getOption(EditorOption.lineHeight).read(reader); - const gutterViewPortPadding = 1; - - // Entire gutter view from top left to bottom right - const gutterWidthWithoutPadding = layout.decorationsLeft + layout.decorationsWidth - layout.glyphMarginLeft - 2 * gutterViewPortPadding; - const gutterHeightWithoutPadding = layout.height - 2 * gutterViewPortPadding; - const gutterViewPortWithStickyScroll = Rect.fromLeftTopWidthHeight(gutterViewPortPadding, gutterViewPortPadding, gutterWidthWithoutPadding, gutterHeightWithoutPadding); - const gutterViewPortWithoutStickyScroll = gutterViewPortWithStickyScroll.withTop(this._stickyScrollHeight.read(reader) + gutterViewPortPadding); - - // The glyph margin area across all relevant lines - const verticalEditRange = s.lineOffsetRange.read(reader); - const gutterEditArea = Rect.fromRanges(OffsetRange.fromTo(gutterViewPortWithoutStickyScroll.left, gutterViewPortWithoutStickyScroll.right), verticalEditRange); - - // The gutter view container (pill) - const pillHeight = lineHeight; - const pillOffset = this._verticalOffset.read(reader); - const pillFullyDockedRect = gutterEditArea.withHeight(pillHeight).translateY(pillOffset); - const pillIsFullyDocked = gutterViewPortWithoutStickyScroll.containsRect(pillFullyDockedRect); - - // The icon which will be rendered in the pill - const iconNoneDocked = this._tabAction.map(action => action === InlineEditTabAction.Accept ? Codicon.keyboardTab : Codicon.arrowRight); - const iconDocked = derived(reader => { - if (this._isHoveredOverIconDebounced.read(reader) || this._isHoveredOverInlineEditDebounced.read(reader)) { - return Codicon.check; - } - if (this._tabAction.read(reader) === InlineEditTabAction.Accept) { - return Codicon.keyboardTab; - } - const cursorLineNumber = this._editorObs.cursorLineNumber.read(reader) ?? 0; - const editStartLineNumber = s.range.read(reader).startLineNumber; - return cursorLineNumber <= editStartLineNumber ? Codicon.keyboardTabAbove : Codicon.keyboardTabBelow; - }); - - const idealIconWidth = 22; - const minimalIconWidth = 16; // codicon size - const iconWidth = (pillRect: Rect) => { - const availableWidth = this._availableWidthForIcon.get()(pillRect.bottom + this._editorObs.editor.getScrollTop()) - gutterViewPortPadding; - return Math.max(Math.min(availableWidth, idealIconWidth), minimalIconWidth); - }; - - if (pillIsFullyDocked) { - const pillRect = pillFullyDockedRect; - const lineNumberWidth = Math.max(layout.lineNumbersLeft + layout.lineNumbersWidth - gutterViewPortWithStickyScroll.left, 0); - const lineNumberRect = pillRect.withWidth(lineNumberWidth); - const iconWidth = Math.max(Math.min(layout.decorationsWidth, idealIconWidth), minimalIconWidth); - const iconRect = pillRect.withWidth(iconWidth).translateX(lineNumberWidth); - - return { - gutterEditArea, - icon: iconDocked, - iconDirection: 'right' as const, - iconRect, - pillRect, - lineNumberRect, - }; - } - - const pillPartiallyDockedPossibleArea = gutterViewPortWithStickyScroll.intersect(gutterEditArea); // The area in which the pill could be partially docked - const pillIsPartiallyDocked = pillPartiallyDockedPossibleArea && pillPartiallyDockedPossibleArea.height >= pillHeight; - - if (pillIsPartiallyDocked) { - // pillFullyDockedRect is outside viewport, move it into the viewport under sticky scroll as we prefer the pill to not be on top of the sticky scroll - // then move it into the possible area which will only cause it to move if it has to be rendered on top of the sticky scroll - const pillRectMoved = pillFullyDockedRect.moveToBeContainedIn(gutterViewPortWithoutStickyScroll).moveToBeContainedIn(pillPartiallyDockedPossibleArea); - const pillRect = pillRectMoved.withWidth(iconWidth(pillRectMoved)); - const iconRect = pillRect; - - return { - gutterEditArea, - icon: iconDocked, - iconDirection: 'right' as const, - iconRect, - pillRect, - }; - } - - // pillFullyDockedRect is outside viewport, so move it into viewport - const pillRectMoved = pillFullyDockedRect.moveToBeContainedIn(gutterViewPortWithStickyScroll); - const pillRect = pillRectMoved.withWidth(iconWidth(pillRectMoved)); - const iconRect = pillRect; - - // docked = pill was already in the viewport - const iconDirection = pillRect.top < pillFullyDockedRect.top ? - 'top' as const : - 'bottom' as const; - - return { - gutterEditArea, - icon: iconNoneDocked, - iconDirection, - iconRect, - pillRect, - }; - }); + private readonly _layout; - private readonly _iconRef = n.ref(); + private readonly _iconRef; - public readonly isVisible = this._layout.map(l => !!l); + public readonly isVisible; - private readonly _hoverVisible = observableValue(this, false); - public readonly isHoverVisible: IObservable = this._hoverVisible; + private readonly _hoverVisible; + public readonly isHoverVisible: IObservable; - private readonly _isHoveredOverIcon = observableValue(this, false); - private readonly _isHoveredOverIconDebounced: IObservable = debouncedObservable(this._isHoveredOverIcon, 100); - public readonly isHoveredOverIcon: IObservable = this._isHoveredOverIconDebounced; + private readonly _isHoveredOverIcon; + private readonly _isHoveredOverIconDebounced: IObservable; + public readonly isHoveredOverIcon: IObservable; private _showHover(): void { if (this._hoverVisible.get()) { @@ -396,91 +497,9 @@ export class InlineEditsGutterIndicator extends Disposable { } } - private readonly _tabAction = derived(this, reader => { - const model = this._model.read(reader); - if (!model) { return InlineEditTabAction.Inactive; } - return model.tabAction.read(reader); - }); + private readonly _tabAction; - private readonly _indicator = n.div({ - class: 'inline-edits-view-gutter-indicator', - onclick: () => { - const layout = this._layout.get(); - const acceptOnClick = layout?.icon.get() === Codicon.check; - - this._editorObs.editor.focus(); - if (acceptOnClick) { - this.model.accept(); - } else { - this.model.jump(); - } - }, - tabIndex: 0, - style: { - position: 'absolute', - overflow: 'visible', - }, - }, mapOutFalsy(this._layout).map(layout => !layout ? [] : [ - n.div({ - style: { - position: 'absolute', - background: asCssVariable(inlineEditIndicatorBackground), - borderRadius: '4px', - ...rectToProps(reader => layout.read(reader).gutterEditArea), - } - }), - n.div({ - class: 'icon', - ref: this._iconRef, - onmouseenter: () => { - // TODO show hover when hovering ghost text etc. - this._showHover(); - }, - style: { - cursor: 'pointer', - zIndex: '20', - position: 'absolute', - backgroundColor: this._gutterIndicatorStyles.map(v => v.background), - ['--vscodeIconForeground' as any]: this._gutterIndicatorStyles.map(v => v.foreground), - border: this._gutterIndicatorStyles.map(v => `1px solid ${v.border}`), - boxSizing: 'border-box', - borderRadius: '4px', - display: 'flex', - justifyContent: 'flex-end', - transition: 'background-color 0.2s ease-in-out, width 0.2s ease-in-out', - ...rectToProps(reader => layout.read(reader).pillRect), - } - }, [ - n.div({ - className: 'line-number', - style: { - lineHeight: layout.map(l => l.lineNumberRect ? l.lineNumberRect.height : 0), - display: layout.map(l => l.lineNumberRect ? 'flex' : 'none'), - alignItems: 'center', - justifyContent: 'flex-end', - width: layout.map(l => l.lineNumberRect ? l.lineNumberRect.width : 0), - height: '100%', - color: this._gutterIndicatorStyles.map(v => v.foreground), - } - }, - this._lineNumberToRender - ), - n.div({ - style: { - rotate: layout.map(l => `${getRotationFromDirection(l.iconDirection)}deg`), - transition: 'rotate 0.2s ease-in-out', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - height: '100%', - marginRight: layout.map(l => l.pillRect.width - l.iconRect.width - (l.lineNumberRect?.width ?? 0)), - width: layout.map(l => l.iconRect.width), - } - }, [ - layout.map((l, reader) => renderIcon(l.icon.read(reader))), - ]) - ]), - ])).keepUpdated(this._store); + private readonly _indicator; } function getRotationFromDirection(direction: 'top' | 'bottom' | 'right'): number { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/indicatorView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/indicatorView.ts index a30da5e93a2..7b54bd6318c 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/indicatorView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/indicatorView.ts @@ -12,7 +12,7 @@ import { localize } from '../../../../../../../nls.js'; import { buttonBackground, buttonForeground, buttonSeparator } from '../../../../../../../platform/theme/common/colorRegistry.js'; import { registerColor } from '../../../../../../../platform/theme/common/colorUtils.js'; import { ObservableCodeEditor } from '../../../../../../browser/observableCodeEditor.js'; -import { OffsetRange } from '../../../../../../common/core/offsetRange.js'; +import { OffsetRange } from '../../../../../../common/core/ranges/offsetRange.js'; import { InlineCompletionsModel } from '../../../model/inlineCompletionsModel.js'; export interface IInlineEditsIndicatorState { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditWithChanges.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditWithChanges.ts index 8c5151cbc9d..cd19acc154f 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditWithChanges.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditWithChanges.ts @@ -6,13 +6,14 @@ import { SingleLineEdit } from '../../../../../common/core/edits/lineEdit.js'; import { LineRange } from '../../../../../common/core/ranges/lineRange.js'; import { Position } from '../../../../../common/core/position.js'; -import { AbstractText, TextEdit } from '../../../../../common/core/edits/textEdit.js'; +import { TextEdit } from '../../../../../common/core/edits/textEdit.js'; +import { AbstractText } from '../../../../../common/core/text/abstractText.js'; import { Command } from '../../../../../common/languages.js'; import { InlineSuggestionItem } from '../../model/inlineSuggestionItem.js'; export class InlineEditWithChanges { public get lineEdit() { - return SingleLineEdit.fromSingleTextEdit(this.edit.toSingle(this.originalText), this.originalText); + return SingleLineEdit.fromSingleTextEdit(this.edit.toReplacement(this.originalText), this.originalText); } public get originalLineRange() { return this.lineEdit.lineRange; } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsModel.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsModel.ts index de7526b0098..2e634565e66 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsModel.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsModel.ts @@ -9,7 +9,8 @@ import { localize } from '../../../../../../nls.js'; import { ICodeEditor } from '../../../../../browser/editorBrowser.js'; import { observableCodeEditor } from '../../../../../browser/observableCodeEditor.js'; import { LineRange } from '../../../../../common/core/ranges/lineRange.js'; -import { StringText, TextEdit } from '../../../../../common/core/edits/textEdit.js'; +import { TextEdit } from '../../../../../common/core/edits/textEdit.js'; +import { StringText } from '../../../../../common/core/text/abstractText.js'; import { Command, InlineCompletionDisplayLocation } from '../../../../../common/languages.js'; import { InlineCompletionsModel } from '../../model/inlineCompletionsModel.js'; import { InlineCompletionItem } from '../../model/inlineSuggestionItem.js'; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsView.ts index d57eccc5990..bd31073f982 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsView.ts @@ -7,7 +7,7 @@ import { equalsIfDefined, itemEquals } from '../../../../../../base/common/equal import { BugIndicatingError } from '../../../../../../base/common/errors.js'; import { Event } from '../../../../../../base/common/event.js'; import { Disposable } from '../../../../../../base/common/lifecycle.js'; -import { autorunWithStore, derived, derivedOpts, derivedWithStore, IObservable, IReader, ISettableObservable, mapObservableArrayCached, observableValue } from '../../../../../../base/common/observable.js'; +import { autorunWithStore, derived, derivedOpts, IObservable, IReader, ISettableObservable, mapObservableArrayCached, observableValue } from '../../../../../../base/common/observable.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { ICodeEditor } from '../../../../../browser/editorBrowser.js'; import { ObservableCodeEditor, observableCodeEditor } from '../../../../../browser/observableCodeEditor.js'; @@ -15,8 +15,9 @@ import { EditorOption } from '../../../../../common/config/editorOptions.js'; import { LineRange } from '../../../../../common/core/ranges/lineRange.js'; import { Position } from '../../../../../common/core/position.js'; import { Range } from '../../../../../common/core/range.js'; -import { AbstractText, SingleTextEdit, StringText } from '../../../../../common/core/edits/textEdit.js'; -import { TextLength } from '../../../../../common/core/textLength.js'; +import { TextReplacement } from '../../../../../common/core/edits/textEdit.js'; +import { AbstractText, StringText } from '../../../../../common/core/text/abstractText.js'; +import { TextLength } from '../../../../../common/core/text/textLength.js'; import { DetailedLineRangeMapping, lineRangeMappingFromRangeMappings, RangeMapping } from '../../../../../common/diff/rangeMapping.js'; import { TextModel } from '../../../../../common/model/textModel.js'; import { InlineEditsGutterIndicator } from './components/gutterIndicatorView.js'; @@ -36,13 +37,13 @@ import { applyEditToModifiedRangeMappings, createReindentEdit } from './utils/ut import './view.css'; export class InlineEditsView extends Disposable { - private readonly _editorObs: ObservableCodeEditor = observableCodeEditor(this._editor); + private readonly _editorObs: ObservableCodeEditor; private readonly _useCodeShifting; private readonly _renderSideBySide; private readonly _useMultiLineGhostText; - private readonly _tabAction = derived(reader => this._model.read(reader)?.tabAction.read(reader) ?? InlineEditTabAction.Inactive); + private readonly _tabAction; private _previousView: { id: string; @@ -60,6 +61,202 @@ export class InlineEditsView extends Disposable { @IInstantiationService private readonly _instantiationService: IInstantiationService, ) { super(); + this._editorObs = observableCodeEditor(this._editor); + this._tabAction = derived(reader => this._model.read(reader)?.tabAction.read(reader) ?? InlineEditTabAction.Inactive); + this._constructorDone = observableValue(this, false); + this._uiState = derived<{ + state: ReturnType; + diff: DetailedLineRangeMapping[]; + edit: InlineEditWithChanges; + newText: string; + newTextLineCount: number; + } | undefined>(this, reader => { + const model = this._model.read(reader); + if (!model || !this._constructorDone.read(reader)) { + return undefined; + } + + model.handleInlineEditShown(); + + const inlineEdit = model.inlineEdit; + let mappings = RangeMapping.fromEdit(inlineEdit.edit); + let newText = inlineEdit.edit.apply(inlineEdit.originalText); + let diff = lineRangeMappingFromRangeMappings(mappings, inlineEdit.originalText, new StringText(newText)); + + let state = this.determineRenderState(model, reader, diff, new StringText(newText)); + if (!state) { + model.abort(`unable to determine view: tried to render ${this._previousView?.view}`); + return undefined; + } + + if (state.kind === 'sideBySide') { + const indentationAdjustmentEdit = createReindentEdit(newText, inlineEdit.modifiedLineRange); + newText = indentationAdjustmentEdit.applyToString(newText); + + mappings = applyEditToModifiedRangeMappings(mappings, indentationAdjustmentEdit); + diff = lineRangeMappingFromRangeMappings(mappings, inlineEdit.originalText, new StringText(newText)); + } + + this._previewTextModel.setLanguage(this._editor.getModel()!.getLanguageId()); + + const previousNewText = this._previewTextModel.getValue(); + if (previousNewText !== newText) { + // Only update the model if the text has changed to avoid flickering + this._previewTextModel.setValue(newText); + } + + if (model.showCollapsed.read(reader) && !this._indicator.read(reader)?.isHoverVisible.read(reader)) { + state = { kind: 'collapsed' }; + } + + return { + state, + diff, + edit: inlineEdit, + newText, + newTextLineCount: inlineEdit.modifiedLineRange.length, + }; + }); + this._previewTextModel = this._register(this._instantiationService.createInstance( + TextModel, + '', + this._editor.getModel()!.getLanguageId(), + { ...TextModel.DEFAULT_CREATION_OPTIONS, bracketPairColorizationOptions: { enabled: true, independentColorPoolPerBracketType: false } }, + null + )); + this._indicatorCyclicDependencyCircuitBreaker = observableValue(this, false); + this._indicator = derived(this, (reader) => { + if (!this._indicatorCyclicDependencyCircuitBreaker.read(reader)) { + return undefined; + } + + const indicatorDisplayRange = derivedOpts({ owner: this, equalsFn: equalsIfDefined(itemEquals()) }, reader => { + const ghostTextIndicator = this._ghostTextIndicator.read(reader); + if (ghostTextIndicator) { + return ghostTextIndicator.lineRange; + } + + const state = this._uiState.read(reader); + if (!state) { return undefined; } + + if (state.state?.kind === 'custom') { + const range = state.state.displayLocation?.range; + if (!range) { + throw new BugIndicatingError('custom view should have a range'); + } + return new LineRange(range.startLineNumber, range.endLineNumber); + } + + if (state.state?.kind === 'insertionMultiLine') { + return this._insertion.originalLines.read(reader); + } + + return state.edit.displayRange; + }); + + const modelWithGhostTextSupport = derived(this, reader => { + const model = this._model.read(reader); + if (model) { + return model; + } + + const ghostTextIndicator = this._ghostTextIndicator.read(reader); + if (ghostTextIndicator) { + return ghostTextIndicator.model; + } + + return model; + }); + + return reader.store.add(this._instantiationService.createInstance( + InlineEditsGutterIndicator, + this._editorObs, + indicatorDisplayRange, + this._gutterIndicatorOffset, + modelWithGhostTextSupport, + this._inlineEditsIsHovered, + this._focusIsInMenu, + )); + }); + this._inlineEditsIsHovered = derived(this, reader => { + return this._sideBySide.isHovered.read(reader) + || this._wordReplacementViews.read(reader).some(v => v.isHovered.read(reader)) + || this._deletion.isHovered.read(reader) + || this._inlineDiffView.isHovered.read(reader) + || this._lineReplacementView.isHovered.read(reader) + || this._insertion.isHovered.read(reader) + || this._customView.isHovered.read(reader); + }); + this._gutterIndicatorOffset = derived(this, reader => { + // TODO: have a better way to tell the gutter indicator view where the edit is inside a viewzone + if (this._uiState.read(reader)?.state?.kind === 'insertionMultiLine') { + return this._insertion.startLineOffset.read(reader); + } + return 0; + }); + this._sideBySide = this._register(this._instantiationService.createInstance(InlineEditsSideBySideView, + this._editor, + this._model.map(m => m?.inlineEdit), + this._previewTextModel, + this._uiState.map(s => s && s.state?.kind === 'sideBySide' ? ({ + newTextLineCount: s.newTextLineCount, + }) : undefined), + this._tabAction, + )); + this._deletion = this._register(this._instantiationService.createInstance(InlineEditsDeletionView, + this._editor, + this._model.map(m => m?.inlineEdit), + this._uiState.map(s => s && s.state?.kind === 'deletion' ? ({ + originalRange: s.state.originalRange, + deletions: s.state.deletions, + }) : undefined), + this._tabAction, + )); + this._insertion = this._register(this._instantiationService.createInstance(InlineEditsInsertionView, + this._editor, + this._uiState.map(s => s && s.state?.kind === 'insertionMultiLine' ? ({ + lineNumber: s.state.lineNumber, + startColumn: s.state.column, + text: s.state.text, + }) : undefined), + this._tabAction, + )); + this._inlineDiffViewState = derived(this, reader => { + const e = this._uiState.read(reader); + if (!e || !e.state) { return undefined; } + if (e.state.kind === 'wordReplacements' || e.state.kind === 'lineReplacement' || e.state.kind === 'insertionMultiLine' || e.state.kind === 'collapsed' || e.state.kind === 'custom') { + return undefined; + } + return { + modifiedText: new StringText(e.newText), + diff: e.diff, + mode: e.state.kind, + modifiedCodeEditor: this._sideBySide.previewEditor, + }; + }); + this._inlineCollapsedView = this._register(this._instantiationService.createInstance(InlineEditsCollapsedView, + this._editor, + this._model.map((m, reader) => this._uiState.read(reader)?.state?.kind === 'collapsed' ? m?.inlineEdit : undefined) + )); + this._customView = this._register(this._instantiationService.createInstance(InlineEditsCustomView, + this._editor, + this._model.map((m, reader) => this._uiState.read(reader)?.state?.kind === 'custom' ? m?.displayLocation : undefined), + this._tabAction, + )); + this._inlineDiffView = this._register(new OriginalEditorInlineDiffView(this._editor, this._inlineDiffViewState, this._previewTextModel)); + this._wordReplacementViews = mapObservableArrayCached(this, this._uiState.map(s => s?.state?.kind === 'wordReplacements' ? s.state.replacements : []), (e, store) => { + return store.add(this._instantiationService.createInstance(InlineEditsWordReplacementView, this._editorObs, e, this._tabAction)); + }); + this._lineReplacementView = this._register(this._instantiationService.createInstance(InlineEditsLineReplacementView, + this._editorObs, + this._uiState.map(s => s?.state?.kind === 'lineReplacement' ? ({ + originalRange: s.state.originalRange, + modifiedRange: s.state.modifiedRange, + modifiedLines: s.state.modifiedLines, + replacements: s.state.replacements, + }) : undefined), + this._tabAction, + )); this._useCodeShifting = this._editorObs.getOption(EditorOption.inlineSuggest).map(s => s.edits.allowCodeShifting); this._renderSideBySide = this._editorObs.getOption(EditorOption.inlineSuggest).map(s => s.edits.renderSideBySide); @@ -99,215 +296,37 @@ export class InlineEditsView extends Disposable { this._constructorDone.set(true, undefined); // TODO: remove and use correct initialization order } - private readonly _constructorDone = observableValue(this, false); + private readonly _constructorDone; - private readonly _uiState = derived<{ - state: ReturnType; - diff: DetailedLineRangeMapping[]; - edit: InlineEditWithChanges; - newText: string; - newTextLineCount: number; - } | undefined>(this, reader => { - const model = this._model.read(reader); - if (!model || !this._constructorDone.read(reader)) { - return undefined; - } + private readonly _uiState; - model.handleInlineEditShown(); + private readonly _previewTextModel; - const inlineEdit = model.inlineEdit; - let mappings = RangeMapping.fromEdit(inlineEdit.edit); - let newText = inlineEdit.edit.apply(inlineEdit.originalText); - let diff = lineRangeMappingFromRangeMappings(mappings, inlineEdit.originalText, new StringText(newText)); + private readonly _indicatorCyclicDependencyCircuitBreaker; - let state = this.determineRenderState(model, reader, diff, new StringText(newText)); - if (!state) { - model.abort(`unable to determine view: tried to render ${this._previousView?.view}`); - return undefined; - } + protected readonly _indicator; - if (state.kind === 'sideBySide') { - const indentationAdjustmentEdit = createReindentEdit(newText, inlineEdit.modifiedLineRange); - newText = indentationAdjustmentEdit.applyToString(newText); + private readonly _inlineEditsIsHovered; - mappings = applyEditToModifiedRangeMappings(mappings, indentationAdjustmentEdit); - diff = lineRangeMappingFromRangeMappings(mappings, inlineEdit.originalText, new StringText(newText)); - } + private readonly _gutterIndicatorOffset; - this._previewTextModel.setLanguage(this._editor.getModel()!.getLanguageId()); + private readonly _sideBySide; - const previousNewText = this._previewTextModel.getValue(); - if (previousNewText !== newText) { - // Only update the model if the text has changed to avoid flickering - this._previewTextModel.setValue(newText); - } + protected readonly _deletion; - if (model.showCollapsed.read(reader) && !this._indicator.read(reader)?.isHoverVisible.read(reader)) { - state = { kind: 'collapsed' }; - } + protected readonly _insertion; - return { - state, - diff, - edit: inlineEdit, - newText, - newTextLineCount: inlineEdit.modifiedLineRange.length, - }; - }); + private readonly _inlineDiffViewState; - private readonly _previewTextModel = this._register(this._instantiationService.createInstance( - TextModel, - '', - this._editor.getModel()!.getLanguageId(), - { ...TextModel.DEFAULT_CREATION_OPTIONS, bracketPairColorizationOptions: { enabled: true, independentColorPoolPerBracketType: false } }, - null - )); + protected readonly _inlineCollapsedView; - private readonly _indicatorCyclicDependencyCircuitBreaker = observableValue(this, false); + protected readonly _customView; - protected readonly _indicator = derivedWithStore(this, (reader, store) => { - if (!this._indicatorCyclicDependencyCircuitBreaker.read(reader)) { - return undefined; - } + protected readonly _inlineDiffView; - const indicatorDisplayRange = derivedOpts({ owner: this, equalsFn: equalsIfDefined(itemEquals()) }, reader => { - const ghostTextIndicator = this._ghostTextIndicator.read(reader); - if (ghostTextIndicator) { - return ghostTextIndicator.lineRange; - } + protected readonly _wordReplacementViews; - const state = this._uiState.read(reader); - if (!state) { return undefined; } - - if (state.state?.kind === 'custom') { - const range = state.state.displayLocation?.range; - if (!range) { - throw new BugIndicatingError('custom view should have a range'); - } - return new LineRange(range.startLineNumber, range.endLineNumber); - } - - if (state.state?.kind === 'insertionMultiLine') { - return this._insertion.originalLines.read(reader); - } - - return state.edit.displayRange; - }); - - const modelWithGhostTextSupport = derived(this, reader => { - const model = this._model.read(reader); - if (model) { - return model; - } - - const ghostTextIndicator = this._ghostTextIndicator.read(reader); - if (ghostTextIndicator) { - return ghostTextIndicator.model; - } - - return model; - }); - - return store.add(this._instantiationService.createInstance( - InlineEditsGutterIndicator, - this._editorObs, - indicatorDisplayRange, - this._gutterIndicatorOffset, - modelWithGhostTextSupport, - this._inlineEditsIsHovered, - this._focusIsInMenu, - )); - }); - - private readonly _inlineEditsIsHovered = derived(this, reader => { - return this._sideBySide.isHovered.read(reader) - || this._wordReplacementViews.read(reader).some(v => v.isHovered.read(reader)) - || this._deletion.isHovered.read(reader) - || this._inlineDiffView.isHovered.read(reader) - || this._lineReplacementView.isHovered.read(reader) - || this._insertion.isHovered.read(reader) - || this._customView.isHovered.read(reader); - }); - - private readonly _gutterIndicatorOffset = derived(this, reader => { - // TODO: have a better way to tell the gutter indicator view where the edit is inside a viewzone - if (this._uiState.read(reader)?.state?.kind === 'insertionMultiLine') { - return this._insertion.startLineOffset.read(reader); - } - return 0; - }); - - private readonly _sideBySide = this._register(this._instantiationService.createInstance(InlineEditsSideBySideView, - this._editor, - this._model.map(m => m?.inlineEdit), - this._previewTextModel, - this._uiState.map(s => s && s.state?.kind === 'sideBySide' ? ({ - newTextLineCount: s.newTextLineCount, - }) : undefined), - this._tabAction, - )); - - protected readonly _deletion = this._register(this._instantiationService.createInstance(InlineEditsDeletionView, - this._editor, - this._model.map(m => m?.inlineEdit), - this._uiState.map(s => s && s.state?.kind === 'deletion' ? ({ - originalRange: s.state.originalRange, - deletions: s.state.deletions, - }) : undefined), - this._tabAction, - )); - - protected readonly _insertion = this._register(this._instantiationService.createInstance(InlineEditsInsertionView, - this._editor, - this._uiState.map(s => s && s.state?.kind === 'insertionMultiLine' ? ({ - lineNumber: s.state.lineNumber, - startColumn: s.state.column, - text: s.state.text, - }) : undefined), - this._tabAction, - )); - - private readonly _inlineDiffViewState = derived(this, reader => { - const e = this._uiState.read(reader); - if (!e || !e.state) { return undefined; } - if (e.state.kind === 'wordReplacements' || e.state.kind === 'lineReplacement' || e.state.kind === 'insertionMultiLine' || e.state.kind === 'collapsed' || e.state.kind === 'custom') { - return undefined; - } - return { - modifiedText: new StringText(e.newText), - diff: e.diff, - mode: e.state.kind, - modifiedCodeEditor: this._sideBySide.previewEditor, - }; - }); - - protected readonly _inlineCollapsedView = this._register(this._instantiationService.createInstance(InlineEditsCollapsedView, - this._editor, - this._model.map((m, reader) => this._uiState.read(reader)?.state?.kind === 'collapsed' ? m?.inlineEdit : undefined) - )); - - protected readonly _customView = this._register(this._instantiationService.createInstance(InlineEditsCustomView, - this._editor, - this._model.map((m, reader) => this._uiState.read(reader)?.state?.kind === 'custom' ? m?.displayLocation : undefined), - this._tabAction, - )); - - protected readonly _inlineDiffView = this._register(new OriginalEditorInlineDiffView(this._editor, this._inlineDiffViewState, this._previewTextModel)); - - protected readonly _wordReplacementViews = mapObservableArrayCached(this, this._uiState.map(s => s?.state?.kind === 'wordReplacements' ? s.state.replacements : []), (e, store) => { - return store.add(this._instantiationService.createInstance(InlineEditsWordReplacementView, this._editorObs, e, this._tabAction)); - }); - - protected readonly _lineReplacementView = this._register(this._instantiationService.createInstance(InlineEditsLineReplacementView, - this._editorObs, - this._uiState.map(s => s?.state?.kind === 'lineReplacement' ? ({ - originalRange: s.state.originalRange, - modifiedRange: s.state.modifiedRange, - modifiedLines: s.state.modifiedLines, - replacements: s.state.replacements, - }) : undefined), - this._tabAction, - )); + protected readonly _lineReplacementView; private getCacheId(model: IInlineEditModel) { return model.inlineEdit.inlineCompletion.identity.id; @@ -359,7 +378,7 @@ export class InlineEditsView extends Disposable { // Make sure there is no insertion, even if we grow them if ( !inner.some(m => m.originalRange.isEmpty()) || - !growEditsUntilWhitespace(inner.map(m => new SingleTextEdit(m.originalRange, '')), inlineEdit.originalText).some(e => e.range.isEmpty() && TextLength.ofRange(e.range).columnCount < InlineEditsWordReplacementView.MAX_LENGTH) + !growEditsUntilWhitespace(inner.map(m => new TextReplacement(m.originalRange, '')), inlineEdit.originalText).some(e => e.range.isEmpty() && TextLength.ofRange(e.range).columnCount < InlineEditsWordReplacementView.MAX_LENGTH) ) { return 'wordReplacements'; } @@ -414,7 +433,7 @@ export class InlineEditsView extends Disposable { }; } - const replacements = inner.map(m => new SingleTextEdit(m.originalRange, newText.getValueOfRange(m.modifiedRange))); + const replacements = inner.map(m => new TextReplacement(m.originalRange, newText.getValueOfRange(m.modifiedRange))); if (replacements.length === 0) { return undefined; } @@ -501,16 +520,16 @@ function isSingleMultiLineInsertion(diff: DetailedLineRangeMapping[]) { return true; } -function growEditsToEntireWord(replacements: SingleTextEdit[], originalText: AbstractText): SingleTextEdit[] { +function growEditsToEntireWord(replacements: TextReplacement[], originalText: AbstractText): TextReplacement[] { return _growEdits(replacements, originalText, (char) => /^[a-zA-Z]$/.test(char)); } -function growEditsUntilWhitespace(replacements: SingleTextEdit[], originalText: AbstractText): SingleTextEdit[] { +function growEditsUntilWhitespace(replacements: TextReplacement[], originalText: AbstractText): TextReplacement[] { return _growEdits(replacements, originalText, (char) => !(/^\s$/.test(char))); } -function _growEdits(replacements: SingleTextEdit[], originalText: AbstractText, fn: (c: string) => boolean): SingleTextEdit[] { - const result: SingleTextEdit[] = []; +function _growEdits(replacements: TextReplacement[], originalText: AbstractText, fn: (c: string) => boolean): TextReplacement[] { + const result: TextReplacement[] = []; replacements.sort((a, b) => Range.compareRangesUsingStarts(a.range, b.range)); @@ -539,9 +558,9 @@ function _growEdits(replacements: SingleTextEdit[], originalText: AbstractText, } // create new edit and merge together if they are touching - let newEdit = new SingleTextEdit(new Range(edit.range.startLineNumber, startIndex + 1, edit.range.endLineNumber, endIndex + 2), prefix + edit.text + suffix); + let newEdit = new TextReplacement(new Range(edit.range.startLineNumber, startIndex + 1, edit.range.endLineNumber, endIndex + 2), prefix + edit.text + suffix); if (result.length > 0 && Range.areIntersectingOrTouching(result[result.length - 1].range, newEdit.range)) { - newEdit = SingleTextEdit.joinEdits([result.pop()!, newEdit], originalText); + newEdit = TextReplacement.joinReplacements([result.pop()!, newEdit], originalText); } result.push(newEdit); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViewProducer.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViewProducer.ts index cd410a053a4..8bd7712e474 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViewProducer.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViewProducer.ts @@ -11,7 +11,7 @@ import { ICodeEditor } from '../../../../../browser/editorBrowser.js'; import { ObservableCodeEditor, observableCodeEditor } from '../../../../../browser/observableCodeEditor.js'; import { LineRange } from '../../../../../common/core/ranges/lineRange.js'; import { Range } from '../../../../../common/core/range.js'; -import { SingleTextEdit, TextEdit } from '../../../../../common/core/edits/textEdit.js'; +import { TextReplacement, TextEdit } from '../../../../../common/core/edits/textEdit.js'; import { TextModelText } from '../../../../../common/model/textModelText.js'; import { InlineCompletionsModel } from '../../model/inlineCompletionsModel.js'; import { InlineEdit } from '../../model/inlineEdit.js'; @@ -36,12 +36,12 @@ export class InlineEditsViewAndDiffProducer extends Disposable { // TODO: This c const editOffset = model.inlineEditState.get()?.inlineCompletion.updatedEdit; if (!editOffset) { return undefined; } - const edits = editOffset.edits.map(e => { + const edits = editOffset.replacements.map(e => { const innerEditRange = Range.fromPositions( textModel.getPositionAt(e.replaceRange.start), textModel.getPositionAt(e.replaceRange.endExclusive) ); - return new SingleTextEdit(innerEditRange, e.newText); + return new TextReplacement(innerEditRange, e.newText); }); const diffEdits = new TextEdit(edits); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/debugVisualization.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/debugVisualization.ts index a316a9a1971..169b6bc6156 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/debugVisualization.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/debugVisualization.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IDisposable } from '../../../../../../../base/common/lifecycle.js'; -import { IReader, derivedWithStore } from '../../../../../../../base/common/observable.js'; +import { IReader, derived } from '../../../../../../../base/common/observable.js'; import { Rect } from '../../../../../../common/core/2d/rect.js'; export interface IVisualizationEffect { @@ -104,8 +104,8 @@ export function debugView(value: unknown, reader: IReader): void { } function debugReadDisposable(d: IDisposable, reader: IReader): void { - derivedWithStore((_reader, store) => { - store.add(d); + derived((_reader) => { + _reader.store.add(d); return undefined; }).read(reader); } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsCollapsedView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsCollapsedView.ts index afddfc1acc6..68683703a68 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsCollapsedView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsCollapsedView.ts @@ -37,7 +37,7 @@ export class InlineEditsCollapsedView extends Disposable implements IInlineEdits this._editorObs = observableCodeEditor(this._editor); - const firstEdit = this._edit.map(inlineEdit => inlineEdit?.edit.edits[0] ?? null); + const firstEdit = this._edit.map(inlineEdit => inlineEdit?.edit.replacements[0] ?? null); const startPosition = firstEdit.map(edit => edit ? singleTextRemoveCommonPrefix(edit, this._editor.getModel()!).range.getStartPosition() : null); const observedStartPoint = this._editorObs.observePosition(startPosition, this._store); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsDeletionView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsDeletionView.ts index 8ea6a771206..6bf12c2b692 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsDeletionView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsDeletionView.ts @@ -14,7 +14,7 @@ import { ObservableCodeEditor, observableCodeEditor } from '../../../../../../br import { Rect } from '../../../../../../common/core/2d/rect.js'; import { EditorOption } from '../../../../../../common/config/editorOptions.js'; import { LineRange } from '../../../../../../common/core/ranges/lineRange.js'; -import { OffsetRange } from '../../../../../../common/core/offsetRange.js'; +import { OffsetRange } from '../../../../../../common/core/ranges/offsetRange.js'; import { Position } from '../../../../../../common/core/position.js'; import { Range } from '../../../../../../common/core/range.js'; import { IInlineEditsView, InlineEditTabAction } from '../inlineEditsViewInterface.js'; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsInsertionView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsInsertionView.ts index 565ef554c0d..fb3eec679d3 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsInsertionView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsInsertionView.ts @@ -6,7 +6,7 @@ import { $, n } from '../../../../../../../base/browser/dom.js'; import { IMouseEvent } from '../../../../../../../base/browser/mouseEvent.js'; import { Emitter } from '../../../../../../../base/common/event.js'; import { Disposable } from '../../../../../../../base/common/lifecycle.js'; -import { constObservable, derived, derivedWithStore, IObservable, observableValue } from '../../../../../../../base/common/observable.js'; +import { constObservable, derived, IObservable, observableValue } from '../../../../../../../base/common/observable.js'; import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js'; import { editorBackground } from '../../../../../../../platform/theme/common/colorRegistry.js'; import { asCssVariable } from '../../../../../../../platform/theme/common/colorUtils.js'; @@ -16,7 +16,7 @@ import { Rect } from '../../../../../../common/core/2d/rect.js'; import { LineSource, renderLines, RenderOptions } from '../../../../../../browser/widget/diffEditor/components/diffEditorViewZones/renderLines.js'; import { EditorOption } from '../../../../../../common/config/editorOptions.js'; import { LineRange } from '../../../../../../common/core/ranges/lineRange.js'; -import { OffsetRange } from '../../../../../../common/core/offsetRange.js'; +import { OffsetRange } from '../../../../../../common/core/ranges/offsetRange.js'; import { Position } from '../../../../../../common/core/position.js'; import { Range } from '../../../../../../common/core/range.js'; import { ILanguageService } from '../../../../../../common/languages/language.js'; @@ -209,7 +209,7 @@ export class InlineEditsInsertionView extends Disposable implements IInlineEdits ) : undefined ); - private readonly _overlayLayout = derivedWithStore(this, (reader, store) => { + private readonly _overlayLayout = derived(this, (reader) => { this._ghostText.read(reader); const state = this._state.read(reader); if (!state) { @@ -217,7 +217,7 @@ export class InlineEditsInsertionView extends Disposable implements IInlineEdits } // Update the overlay when the position changes - this._editorObs.observePosition(observableValue(this, new Position(state.lineNumber, state.column)), store).read(reader); + this._editorObs.observePosition(observableValue(this, new Position(state.lineNumber, state.column)), reader.store).read(reader); const editorLayout = this._editorObs.layoutInfo.read(reader); const horizontalScrollOffset = this._editorObs.scrollLeft.read(reader); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsLineReplacementView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsLineReplacementView.ts index 9a2021b6853..ba71ba07a2a 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsLineReplacementView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsLineReplacementView.ts @@ -19,7 +19,7 @@ import { Rect } from '../../../../../../common/core/2d/rect.js'; import { LineSource, renderLines, RenderOptions } from '../../../../../../browser/widget/diffEditor/components/diffEditorViewZones/renderLines.js'; import { EditorOption } from '../../../../../../common/config/editorOptions.js'; import { LineRange } from '../../../../../../common/core/ranges/lineRange.js'; -import { OffsetRange } from '../../../../../../common/core/offsetRange.js'; +import { OffsetRange } from '../../../../../../common/core/ranges/offsetRange.js'; import { Range } from '../../../../../../common/core/range.js'; import { ILanguageService } from '../../../../../../common/languages/language.js'; import { IModelDecorationOptions, TrackedRangeStickiness } from '../../../../../../common/model.js'; @@ -32,238 +32,24 @@ import { getPrefixTrim, mapOutFalsy, rectToProps } from '../utils/utils.js'; export class InlineEditsLineReplacementView extends Disposable implements IInlineEditsView { - private readonly _onDidClick = this._register(new Emitter()); - readonly onDidClick = this._onDidClick.event; + private readonly _onDidClick; + readonly onDidClick; - private readonly _originalBubblesDecorationCollection = this._editor.editor.createDecorationsCollection(); - private readonly _originalBubblesDecorationOptions: IModelDecorationOptions = { - description: 'inlineCompletions-original-bubble', - className: 'inlineCompletions-original-bubble', - stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges - }; + private readonly _originalBubblesDecorationCollection; + private readonly _originalBubblesDecorationOptions: IModelDecorationOptions; - private readonly _maxPrefixTrim = this._edit.map(e => e ? getPrefixTrim(e.replacements.flatMap(r => [r.originalRange, r.modifiedRange]), e.originalRange, e.modifiedLines, this._editor.editor) : undefined); + private readonly _maxPrefixTrim; - private readonly _modifiedLineElements = derived(reader => { - const lines = []; - let requiredWidth = 0; - - const prefixTrim = this._maxPrefixTrim.read(reader); - const edit = this._edit.read(reader); - if (!edit || !prefixTrim) { - return undefined; - } - - const maxPrefixTrim = prefixTrim.prefixTrim; - const modifiedBubbles = rangesToBubbleRanges(edit.replacements.map(r => r.modifiedRange)).map(r => new Range(r.startLineNumber, r.startColumn - maxPrefixTrim, r.endLineNumber, r.endColumn - maxPrefixTrim)); - - const textModel = this._editor.model.get()!; - const startLineNumber = edit.modifiedRange.startLineNumber; - for (let i = 0; i < edit.modifiedRange.length; i++) { - const line = document.createElement('div'); - const lineNumber = startLineNumber + i; - const modLine = edit.modifiedLines[i].slice(maxPrefixTrim); - - const t = textModel.tokenization.tokenizeLinesAt(lineNumber, [modLine])?.[0]; - let tokens: LineTokens; - if (t) { - tokens = TokenArray.fromLineTokens(t).toLineTokens(modLine, this._languageService.languageIdCodec); - } else { - tokens = LineTokens.createEmpty(modLine, this._languageService.languageIdCodec); - } - - // Inline decorations are broken down into individual spans. To be able to render rounded corners, we need to set the start and end decorations separately. - const decorations = []; - for (const modified of modifiedBubbles.filter(b => b.startLineNumber === lineNumber)) { - const validatedEndColumn = Math.min(modified.endColumn, modLine.length + 1); - decorations.push(new InlineDecoration(new Range(1, modified.startColumn, 1, validatedEndColumn), 'inlineCompletions-modified-bubble', InlineDecorationType.Regular)); - decorations.push(new InlineDecoration(new Range(1, modified.startColumn, 1, modified.startColumn + 1), 'start', InlineDecorationType.Regular)); - decorations.push(new InlineDecoration(new Range(1, validatedEndColumn - 1, 1, validatedEndColumn), 'end', InlineDecorationType.Regular)); - } - - // TODO: All lines should be rendered at once for one dom element - const result = renderLines(new LineSource([tokens]), RenderOptions.fromEditor(this._editor.editor).withSetWidth(false).withScrollBeyondLastColumn(0), decorations, line, true); - this._editor.getOption(EditorOption.fontInfo).read(reader); // update when font info changes - - requiredWidth = Math.max(requiredWidth, result.minWidthInPx); - - lines.push(line); - } - - return { lines, requiredWidth: requiredWidth }; - }); + private readonly _modifiedLineElements; - private readonly _layout = derived(this, reader => { - const modifiedLines = this._modifiedLineElements.read(reader); - const maxPrefixTrim = this._maxPrefixTrim.read(reader); - const edit = this._edit.read(reader); - if (!modifiedLines || !maxPrefixTrim || !edit) { - return undefined; - } + private readonly _layout; - const { prefixLeftOffset } = maxPrefixTrim; - const { requiredWidth } = modifiedLines; + private readonly _viewZoneInfo; - const lineHeight = this._editor.getOption(EditorOption.lineHeight).read(reader); - const contentLeft = this._editor.layoutInfoContentLeft.read(reader); - const verticalScrollbarWidth = this._editor.layoutInfoVerticalScrollbarWidth.read(reader); - const scrollLeft = this._editor.scrollLeft.read(reader); - const scrollTop = this._editor.scrollTop.read(reader); - const editorLeftOffset = contentLeft - scrollLeft; + private readonly _div; - const textModel = this._editor.editor.getModel()!; - - const originalLineWidths = edit.originalRange.mapToLineArray(line => this._editor.editor.getOffsetForColumn(line, textModel.getLineMaxColumn(line)) - prefixLeftOffset); - const maxLineWidth = Math.max(...originalLineWidths, requiredWidth); - - const startLineNumber = edit.originalRange.startLineNumber; - const endLineNumber = edit.originalRange.endLineNumberExclusive - 1; - const topOfOriginalLines = this._editor.editor.getTopForLineNumber(startLineNumber) - scrollTop; - const bottomOfOriginalLines = this._editor.editor.getBottomForLineNumber(endLineNumber) - scrollTop; - - // Box Widget positioning - const originalLinesOverlay = Rect.fromLeftTopWidthHeight( - editorLeftOffset + prefixLeftOffset, - topOfOriginalLines, - maxLineWidth, - bottomOfOriginalLines - topOfOriginalLines - ); - const modifiedLinesOverlay = Rect.fromLeftTopWidthHeight( - originalLinesOverlay.left, - originalLinesOverlay.bottom, - originalLinesOverlay.width, - edit.modifiedRange.length * lineHeight - ); - const background = Rect.hull([originalLinesOverlay, modifiedLinesOverlay]); - - const lowerBackground = background.intersectVertical(new OffsetRange(originalLinesOverlay.bottom, Number.MAX_SAFE_INTEGER)); - const lowerText = new Rect(lowerBackground.left, lowerBackground.top, lowerBackground.right, lowerBackground.bottom); - - return { - originalLinesOverlay, - modifiedLinesOverlay, - background, - lowerBackground, - lowerText, - minContentWidthRequired: prefixLeftOffset + maxLineWidth + verticalScrollbarWidth, - }; - }); - - private readonly _viewZoneInfo = derived<{ height: number; lineNumber: number } | undefined>(reader => { - const shouldShowViewZone = this._editor.getOption(EditorOption.inlineSuggest).map(o => o.edits.allowCodeShifting === 'always').read(reader); - if (!shouldShowViewZone) { - return undefined; - } - - const layout = this._layout.read(reader); - const edit = this._edit.read(reader); - if (!layout || !edit) { - return undefined; - } - - const viewZoneHeight = layout.lowerBackground.height; - const viewZoneLineNumber = edit.originalRange.endLineNumberExclusive; - return { height: viewZoneHeight, lineNumber: viewZoneLineNumber }; - }); - - private readonly _div = n.div({ - class: 'line-replacement', - }, [ - derived(reader => { - const layout = mapOutFalsy(this._layout).read(reader); - const modifiedLineElements = this._modifiedLineElements.read(reader); - if (!layout || !modifiedLineElements) { - return []; - } - - const layoutProps = layout.read(reader); - const contentLeft = this._editor.layoutInfoContentLeft.read(reader); - const contentWidth = this._editor.contentWidth.read(reader); - const contentHeight = this._editor.editor.getContentHeight(); - - const lineHeight = this._editor.getOption(EditorOption.lineHeight).read(reader); - modifiedLineElements.lines.forEach(l => { - l.style.width = `${layoutProps.lowerText.width}px`; - l.style.height = `${lineHeight}px`; - l.style.position = 'relative'; - }); - - const modifiedBorderColor = getModifiedBorderColor(this._tabAction).read(reader); - const originalBorderColor = getOriginalBorderColor(this._tabAction).read(reader); - - return [ - n.div({ - style: { - position: 'absolute', - top: 0, - left: contentLeft, - width: contentWidth, - height: contentHeight, - overflow: 'hidden', - pointerEvents: 'none', - } - }, [ - n.div({ - class: 'originalOverlayLineReplacement', - style: { - position: 'absolute', - ...rectToProps(reader => layout.read(reader).background.translateX(-contentLeft)), - borderRadius: '4px', - - border: getEditorBlendedColor(originalBorderColor, this._themeService).map(c => `1px solid ${c.toString()}`), - pointerEvents: 'none', - boxSizing: 'border-box', - background: asCssVariable(originalBackgroundColor), - } - }), - n.div({ - class: 'modifiedOverlayLineReplacement', - style: { - position: 'absolute', - ...rectToProps(reader => layout.read(reader).lowerBackground.translateX(-contentLeft)), - borderRadius: '0 0 4px 4px', - background: asCssVariable(editorBackground), - boxShadow: `${asCssVariable(scrollbarShadow)} 0 6px 6px -6px`, - border: `1px solid ${asCssVariable(modifiedBorderColor)}`, - boxSizing: 'border-box', - overflow: 'hidden', - cursor: 'pointer', - pointerEvents: 'auto', - }, - onmousedown: e => { - e.preventDefault(); // This prevents that the editor loses focus - }, - onclick: (e) => this._onDidClick.fire(new StandardMouseEvent(getWindow(e), e)), - }, [ - n.div({ - style: { - position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', - background: asCssVariable(modifiedChangedLineBackgroundColor), - }, - }) - ]), - n.div({ - class: 'modifiedLinesLineReplacement', - style: { - position: 'absolute', - boxSizing: 'border-box', - ...rectToProps(reader => layout.read(reader).lowerText.translateX(-contentLeft)), - fontFamily: this._editor.getOption(EditorOption.fontFamily), - fontSize: this._editor.getOption(EditorOption.fontSize), - fontWeight: this._editor.getOption(EditorOption.fontWeight), - pointerEvents: 'none', - whiteSpace: 'nowrap', - borderRadius: '0 0 4px 4px', - overflow: 'hidden', - } - }, [...modifiedLineElements.lines]), - ]) - ]; - }) - ]).keepUpdated(this._store); - - readonly isHovered = this._editor.isTargetHovered((e) => this._isMouseOverWidget(e), this._store); + readonly isHovered; constructor( private readonly _editor: ObservableCodeEditor, @@ -278,6 +64,231 @@ export class InlineEditsLineReplacementView extends Disposable implements IInlin @IThemeService private readonly _themeService: IThemeService, ) { super(); + this._onDidClick = this._register(new Emitter()); + this.onDidClick = this._onDidClick.event; + this._originalBubblesDecorationCollection = this._editor.editor.createDecorationsCollection(); + this._originalBubblesDecorationOptions = { + description: 'inlineCompletions-original-bubble', + className: 'inlineCompletions-original-bubble', + stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges + }; + this._maxPrefixTrim = this._edit.map(e => e ? getPrefixTrim(e.replacements.flatMap(r => [r.originalRange, r.modifiedRange]), e.originalRange, e.modifiedLines, this._editor.editor) : undefined); + this._modifiedLineElements = derived(reader => { + const lines = []; + let requiredWidth = 0; + + const prefixTrim = this._maxPrefixTrim.read(reader); + const edit = this._edit.read(reader); + if (!edit || !prefixTrim) { + return undefined; + } + + const maxPrefixTrim = prefixTrim.prefixTrim; + const modifiedBubbles = rangesToBubbleRanges(edit.replacements.map(r => r.modifiedRange)).map(r => new Range(r.startLineNumber, r.startColumn - maxPrefixTrim, r.endLineNumber, r.endColumn - maxPrefixTrim)); + + const textModel = this._editor.model.get()!; + const startLineNumber = edit.modifiedRange.startLineNumber; + for (let i = 0; i < edit.modifiedRange.length; i++) { + const line = document.createElement('div'); + const lineNumber = startLineNumber + i; + const modLine = edit.modifiedLines[i].slice(maxPrefixTrim); + + const t = textModel.tokenization.tokenizeLinesAt(lineNumber, [modLine])?.[0]; + let tokens: LineTokens; + if (t) { + tokens = TokenArray.fromLineTokens(t).toLineTokens(modLine, this._languageService.languageIdCodec); + } else { + tokens = LineTokens.createEmpty(modLine, this._languageService.languageIdCodec); + } + + // Inline decorations are broken down into individual spans. To be able to render rounded corners, we need to set the start and end decorations separately. + const decorations = []; + for (const modified of modifiedBubbles.filter(b => b.startLineNumber === lineNumber)) { + const validatedEndColumn = Math.min(modified.endColumn, modLine.length + 1); + decorations.push(new InlineDecoration(new Range(1, modified.startColumn, 1, validatedEndColumn), 'inlineCompletions-modified-bubble', InlineDecorationType.Regular)); + decorations.push(new InlineDecoration(new Range(1, modified.startColumn, 1, modified.startColumn + 1), 'start', InlineDecorationType.Regular)); + decorations.push(new InlineDecoration(new Range(1, validatedEndColumn - 1, 1, validatedEndColumn), 'end', InlineDecorationType.Regular)); + } + + // TODO: All lines should be rendered at once for one dom element + const result = renderLines(new LineSource([tokens]), RenderOptions.fromEditor(this._editor.editor).withSetWidth(false).withScrollBeyondLastColumn(0), decorations, line, true); + this._editor.getOption(EditorOption.fontInfo).read(reader); // update when font info changes + + requiredWidth = Math.max(requiredWidth, result.minWidthInPx); + + lines.push(line); + } + + return { lines, requiredWidth: requiredWidth }; + }); + this._layout = derived(this, reader => { + const modifiedLines = this._modifiedLineElements.read(reader); + const maxPrefixTrim = this._maxPrefixTrim.read(reader); + const edit = this._edit.read(reader); + if (!modifiedLines || !maxPrefixTrim || !edit) { + return undefined; + } + + const { prefixLeftOffset } = maxPrefixTrim; + const { requiredWidth } = modifiedLines; + + const lineHeight = this._editor.getOption(EditorOption.lineHeight).read(reader); + const contentLeft = this._editor.layoutInfoContentLeft.read(reader); + const verticalScrollbarWidth = this._editor.layoutInfoVerticalScrollbarWidth.read(reader); + const scrollLeft = this._editor.scrollLeft.read(reader); + const scrollTop = this._editor.scrollTop.read(reader); + const editorLeftOffset = contentLeft - scrollLeft; + + const textModel = this._editor.editor.getModel()!; + + const originalLineWidths = edit.originalRange.mapToLineArray(line => this._editor.editor.getOffsetForColumn(line, textModel.getLineMaxColumn(line)) - prefixLeftOffset); + const maxLineWidth = Math.max(...originalLineWidths, requiredWidth); + + const startLineNumber = edit.originalRange.startLineNumber; + const endLineNumber = edit.originalRange.endLineNumberExclusive - 1; + const topOfOriginalLines = this._editor.editor.getTopForLineNumber(startLineNumber) - scrollTop; + const bottomOfOriginalLines = this._editor.editor.getBottomForLineNumber(endLineNumber) - scrollTop; + + // Box Widget positioning + const originalLinesOverlay = Rect.fromLeftTopWidthHeight( + editorLeftOffset + prefixLeftOffset, + topOfOriginalLines, + maxLineWidth, + bottomOfOriginalLines - topOfOriginalLines + ); + const modifiedLinesOverlay = Rect.fromLeftTopWidthHeight( + originalLinesOverlay.left, + originalLinesOverlay.bottom, + originalLinesOverlay.width, + edit.modifiedRange.length * lineHeight + ); + const background = Rect.hull([originalLinesOverlay, modifiedLinesOverlay]); + + const lowerBackground = background.intersectVertical(new OffsetRange(originalLinesOverlay.bottom, Number.MAX_SAFE_INTEGER)); + const lowerText = new Rect(lowerBackground.left, lowerBackground.top, lowerBackground.right, lowerBackground.bottom); + + return { + originalLinesOverlay, + modifiedLinesOverlay, + background, + lowerBackground, + lowerText, + minContentWidthRequired: prefixLeftOffset + maxLineWidth + verticalScrollbarWidth, + }; + }); + this._viewZoneInfo = derived<{ height: number; lineNumber: number } | undefined>(reader => { + const shouldShowViewZone = this._editor.getOption(EditorOption.inlineSuggest).map(o => o.edits.allowCodeShifting === 'always').read(reader); + if (!shouldShowViewZone) { + return undefined; + } + + const layout = this._layout.read(reader); + const edit = this._edit.read(reader); + if (!layout || !edit) { + return undefined; + } + + const viewZoneHeight = layout.lowerBackground.height; + const viewZoneLineNumber = edit.originalRange.endLineNumberExclusive; + return { height: viewZoneHeight, lineNumber: viewZoneLineNumber }; + }); + this._div = n.div({ + class: 'line-replacement', + }, [ + derived(reader => { + const layout = mapOutFalsy(this._layout).read(reader); + const modifiedLineElements = this._modifiedLineElements.read(reader); + if (!layout || !modifiedLineElements) { + return []; + } + + const layoutProps = layout.read(reader); + const contentLeft = this._editor.layoutInfoContentLeft.read(reader); + const contentWidth = this._editor.contentWidth.read(reader); + const contentHeight = this._editor.editor.getContentHeight(); + + const lineHeight = this._editor.getOption(EditorOption.lineHeight).read(reader); + modifiedLineElements.lines.forEach(l => { + l.style.width = `${layoutProps.lowerText.width}px`; + l.style.height = `${lineHeight}px`; + l.style.position = 'relative'; + }); + + const modifiedBorderColor = getModifiedBorderColor(this._tabAction).read(reader); + const originalBorderColor = getOriginalBorderColor(this._tabAction).read(reader); + + return [ + n.div({ + style: { + position: 'absolute', + top: 0, + left: contentLeft, + width: contentWidth, + height: contentHeight, + overflow: 'hidden', + pointerEvents: 'none', + } + }, [ + n.div({ + class: 'originalOverlayLineReplacement', + style: { + position: 'absolute', + ...rectToProps(reader => layout.read(reader).background.translateX(-contentLeft)), + borderRadius: '4px', + + border: getEditorBlendedColor(originalBorderColor, this._themeService).map(c => `1px solid ${c.toString()}`), + pointerEvents: 'none', + boxSizing: 'border-box', + background: asCssVariable(originalBackgroundColor), + } + }), + n.div({ + class: 'modifiedOverlayLineReplacement', + style: { + position: 'absolute', + ...rectToProps(reader => layout.read(reader).lowerBackground.translateX(-contentLeft)), + borderRadius: '0 0 4px 4px', + background: asCssVariable(editorBackground), + boxShadow: `${asCssVariable(scrollbarShadow)} 0 6px 6px -6px`, + border: `1px solid ${asCssVariable(modifiedBorderColor)}`, + boxSizing: 'border-box', + overflow: 'hidden', + cursor: 'pointer', + pointerEvents: 'auto', + }, + onmousedown: e => { + e.preventDefault(); // This prevents that the editor loses focus + }, + onclick: (e) => this._onDidClick.fire(new StandardMouseEvent(getWindow(e), e)), + }, [ + n.div({ + style: { + position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', + background: asCssVariable(modifiedChangedLineBackgroundColor), + }, + }) + ]), + n.div({ + class: 'modifiedLinesLineReplacement', + style: { + position: 'absolute', + boxSizing: 'border-box', + ...rectToProps(reader => layout.read(reader).lowerText.translateX(-contentLeft)), + fontFamily: this._editor.getOption(EditorOption.fontFamily), + fontSize: this._editor.getOption(EditorOption.fontSize), + fontWeight: this._editor.getOption(EditorOption.fontWeight), + pointerEvents: 'none', + whiteSpace: 'nowrap', + borderRadius: '0 0 4px 4px', + overflow: 'hidden', + } + }, [...modifiedLineElements.lines]), + ]) + ]; + }) + ]).keepUpdated(this._store); + this.isHovered = this._editor.isTargetHovered((e) => this._isMouseOverWidget(e), this._store); + this._previousViewZoneInfo = undefined; this._register(toDisposable(() => this._originalBubblesDecorationCollection.clear())); this._register(toDisposable(() => this._editor.editor.changeViewZones(accessor => this.removePreviousViewZone(accessor)))); @@ -322,7 +333,7 @@ export class InlineEditsLineReplacementView extends Disposable implements IInlin } // View Zones - private _previousViewZoneInfo: { height: number; lineNumber: number; id: string } | undefined = undefined; + private _previousViewZoneInfo: { height: number; lineNumber: number; id: string } | undefined; private removePreviousViewZone(changeAccessor: IViewZoneChangeAccessor) { if (!this._previousViewZoneInfo) { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsSideBySideView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsSideBySideView.ts index e06415d014c..592be28800f 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsSideBySideView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsSideBySideView.ts @@ -17,7 +17,7 @@ import { observableCodeEditor } from '../../../../../../browser/observableCodeEd import { Rect } from '../../../../../../common/core/2d/rect.js'; import { EmbeddedCodeEditorWidget } from '../../../../../../browser/widget/codeEditor/embeddedCodeEditorWidget.js'; import { EditorOption } from '../../../../../../common/config/editorOptions.js'; -import { OffsetRange } from '../../../../../../common/core/offsetRange.js'; +import { OffsetRange } from '../../../../../../common/core/ranges/offsetRange.js'; import { Position } from '../../../../../../common/core/position.js'; import { Range } from '../../../../../../common/core/range.js'; import { ITextModel } from '../../../../../../common/model.js'; @@ -56,10 +56,10 @@ export class InlineEditsSideBySideView extends Disposable implements IInlineEdit return maxOriginalContent + maxModifiedContent + originalPadding + modifiedPadding < editorWidth - editorContentLeft - editorVerticalScrollbar - minimapWidth; } - private readonly _editorObs = observableCodeEditor(this._editor); + private readonly _editorObs; - private readonly _onDidClick = this._register(new Emitter()); - readonly onDidClick = this._onDidClick.event; + private readonly _onDidClick; + readonly onDidClick; constructor( private readonly _editor: ICodeEditor, @@ -73,6 +73,463 @@ export class InlineEditsSideBySideView extends Disposable implements IInlineEdit @IThemeService private readonly _themeService: IThemeService, ) { super(); + this._editorObs = observableCodeEditor(this._editor); + this._onDidClick = this._register(new Emitter()); + this.onDidClick = this._onDidClick.event; + this._display = derived(this, reader => !!this._uiState.read(reader) ? 'block' : 'none'); + this.previewRef = n.ref(); + this._editorContainer = n.div({ + class: ['editorContainer'], + style: { position: 'absolute', overflow: 'hidden', cursor: 'pointer' }, + onmousedown: e => { + e.preventDefault(); // This prevents that the editor loses focus + }, + onclick: (e) => { + this._onDidClick.fire(new StandardMouseEvent(getWindow(e), e)); + } + }, [ + n.div({ class: 'preview', style: { pointerEvents: 'none' }, ref: this.previewRef }), + ]).keepUpdated(this._store); + this.isHovered = this._editorContainer.didMouseMoveDuringHover; + this.previewEditor = this._register(this._instantiationService.createInstance( + EmbeddedCodeEditorWidget, + this.previewRef.element, + { + glyphMargin: false, + lineNumbers: 'off', + minimap: { enabled: false }, + guides: { + indentation: false, + bracketPairs: false, + bracketPairsHorizontal: false, + highlightActiveIndentation: false, + }, + rulers: [], + padding: { top: 0, bottom: 0 }, + folding: false, + selectOnLineNumbers: false, + selectionHighlight: false, + columnSelection: false, + overviewRulerBorder: false, + overviewRulerLanes: 0, + lineDecorationsWidth: 0, + lineNumbersMinChars: 0, + revealHorizontalRightPadding: 0, + bracketPairColorization: { enabled: true, independentColorPoolPerBracketType: false }, + scrollBeyondLastLine: false, + scrollbar: { + vertical: 'hidden', + horizontal: 'hidden', + handleMouseWheel: false, + }, + readOnly: true, + wordWrap: 'off', + wordWrapOverride1: 'off', + wordWrapOverride2: 'off', + }, + { + contextKeyValues: { + [InlineCompletionContextKeys.inInlineEditsPreviewEditor.key]: true, + }, + contributions: [], + }, + this._editor + )); + this._previewEditorObs = observableCodeEditor(this.previewEditor); + this._activeViewZones = []; + this._updatePreviewEditor = derived(reader => { + this._editorContainer.readEffect(reader); + this._previewEditorObs.model.read(reader); // update when the model is set + + // Setting this here explicitly to make sure that the preview editor is + // visible when needed, we're also checking that these fields are defined + // because of the auto run initial + // Before removing these, verify with a non-monospace font family + this._display.read(reader); + if (this._nonOverflowView) { + this._nonOverflowView.element.style.display = this._display.read(reader); + } + + const uiState = this._uiState.read(reader); + const edit = this._edit.read(reader); + if (!uiState || !edit) { + return; + } + + const range = edit.originalLineRange; + + const hiddenAreas: Range[] = []; + if (range.startLineNumber > 1) { + hiddenAreas.push(new Range(1, 1, range.startLineNumber - 1, 1)); + } + if (range.startLineNumber + uiState.newTextLineCount < this._previewTextModel.getLineCount() + 1) { + hiddenAreas.push(new Range(range.startLineNumber + uiState.newTextLineCount, 1, this._previewTextModel.getLineCount() + 1, 1)); + } + + this.previewEditor.setHiddenAreas(hiddenAreas, undefined, true); + + // TODO: is this the proper way to handle viewzones? + const previousViewZones = [...this._activeViewZones]; + this._activeViewZones = []; + + const reducedLinesCount = (range.endLineNumberExclusive - range.startLineNumber) - uiState.newTextLineCount; + this.previewEditor.changeViewZones((changeAccessor) => { + previousViewZones.forEach(id => changeAccessor.removeZone(id)); + + if (reducedLinesCount > 0) { + this._activeViewZones.push(changeAccessor.addZone({ + afterLineNumber: range.startLineNumber + uiState.newTextLineCount - 1, + heightInLines: reducedLinesCount, + showInHiddenAreas: true, + domNode: $('div.diagonal-fill.inline-edits-view-zone'), + })); + } + }); + }); + this._previewEditorWidth = derived(this, reader => { + const edit = this._edit.read(reader); + if (!edit) { return 0; } + this._updatePreviewEditor.read(reader); + + return maxContentWidthInRange(this._previewEditorObs, edit.modifiedLineRange, reader); + }); + this._cursorPosIfTouchesEdit = derived(this, reader => { + const cursorPos = this._editorObs.cursorPosition.read(reader); + const edit = this._edit.read(reader); + if (!edit || !cursorPos) { return undefined; } + return edit.modifiedLineRange.contains(cursorPos.lineNumber) ? cursorPos : undefined; + }); + this._originalStartPosition = derived(this, (reader) => { + const inlineEdit = this._edit.read(reader); + return inlineEdit ? new Position(inlineEdit.originalLineRange.startLineNumber, 1) : null; + }); + this._originalEndPosition = derived(this, (reader) => { + const inlineEdit = this._edit.read(reader); + return inlineEdit ? new Position(inlineEdit.originalLineRange.endLineNumberExclusive, 1) : null; + }); + this._originalVerticalStartPosition = this._editorObs.observePosition(this._originalStartPosition, this._store).map(p => p?.y); + this._originalVerticalEndPosition = this._editorObs.observePosition(this._originalEndPosition, this._store).map(p => p?.y); + this._originalDisplayRange = this._edit.map(e => e?.displayRange); + this._editorMaxContentWidthInRange = derived(this, reader => { + const originalDisplayRange = this._originalDisplayRange.read(reader); + if (!originalDisplayRange) { + return constObservable(0); + } + this._editorObs.versionId.read(reader); + + // Take the max value that we observed. + // Reset when either the edit changes or the editor text version. + return derivedObservableWithCache(this, (reader, lastValue) => { + const maxWidth = maxContentWidthInRange(this._editorObs, originalDisplayRange, reader); + return Math.max(maxWidth, lastValue ?? 0); + }); + }).map((v, r) => v.read(r)); + this._previewEditorLayoutInfo = derived(this, (reader) => { + const inlineEdit = this._edit.read(reader); + if (!inlineEdit) { + return null; + } + const state = this._uiState.read(reader); + if (!state) { + return null; + } + + const range = inlineEdit.originalLineRange; + + const horizontalScrollOffset = this._editorObs.scrollLeft.read(reader); + + const editorContentMaxWidthInRange = this._editorMaxContentWidthInRange.read(reader); + const editorLayout = this._editorObs.layoutInfo.read(reader); + const previewContentWidth = this._previewEditorWidth.read(reader); + const editorContentAreaWidth = editorLayout.contentWidth - editorLayout.verticalScrollbarWidth; + const editorBoundingClientRect = this._editor.getContainerDomNode().getBoundingClientRect(); + const clientContentAreaRight = editorLayout.contentLeft + editorLayout.contentWidth + editorBoundingClientRect.left; + const remainingWidthRightOfContent = getWindow(this._editor.getContainerDomNode()).innerWidth - clientContentAreaRight; + const remainingWidthRightOfEditor = getWindow(this._editor.getContainerDomNode()).innerWidth - editorBoundingClientRect.right; + const desiredMinimumWidth = Math.min(editorLayout.contentWidth * 0.3, previewContentWidth, 100); + const IN_EDITOR_DISPLACEMENT = 0; + const maximumAvailableWidth = IN_EDITOR_DISPLACEMENT + remainingWidthRightOfContent; + + const cursorPos = this._cursorPosIfTouchesEdit.read(reader); + + const maxPreviewEditorLeft = Math.max( + // We're starting from the content area right and moving it left by IN_EDITOR_DISPLACEMENT and also by an amount to ensure some minimum desired width + editorContentAreaWidth + horizontalScrollOffset - IN_EDITOR_DISPLACEMENT - Math.max(0, desiredMinimumWidth - maximumAvailableWidth), + // But we don't want that the moving left ends up covering the cursor, so this will push it to the right again + Math.min( + cursorPos ? getOffsetForPos(this._editorObs, cursorPos, reader) + 50 : 0, + editorContentAreaWidth + horizontalScrollOffset + ) + ); + const previewEditorLeftInTextArea = Math.min(editorContentMaxWidthInRange + ORIGINAL_END_PADDING, maxPreviewEditorLeft); + + const maxContentWidth = editorContentMaxWidthInRange + ORIGINAL_END_PADDING + previewContentWidth + 70; + + const dist = maxPreviewEditorLeft - previewEditorLeftInTextArea; + + let desiredPreviewEditorScrollLeft; + let codeRight; + if (previewEditorLeftInTextArea > horizontalScrollOffset) { + desiredPreviewEditorScrollLeft = 0; + codeRight = editorLayout.contentLeft + previewEditorLeftInTextArea - horizontalScrollOffset; + } else { + desiredPreviewEditorScrollLeft = horizontalScrollOffset - previewEditorLeftInTextArea; + codeRight = editorLayout.contentLeft; + } + + const selectionTop = this._originalVerticalStartPosition.read(reader) ?? this._editor.getTopForLineNumber(range.startLineNumber) - this._editorObs.scrollTop.read(reader); + const selectionBottom = this._originalVerticalEndPosition.read(reader) ?? this._editor.getBottomForLineNumber(range.endLineNumberExclusive - 1) - this._editorObs.scrollTop.read(reader); + + // TODO: const { prefixLeftOffset } = getPrefixTrim(inlineEdit.edit.edits.map(e => e.range), inlineEdit.originalLineRange, [], this._editor); + const codeLeft = editorLayout.contentLeft - horizontalScrollOffset; + + let codeRect = Rect.fromLeftTopRightBottom(codeLeft, selectionTop, codeRight, selectionBottom); + const isInsertion = codeRect.height === 0; + if (!isInsertion) { + codeRect = codeRect.withMargin(VERTICAL_PADDING, HORIZONTAL_PADDING); + } + + const editHeight = this._editor.getOption(EditorOption.lineHeight) * inlineEdit.modifiedLineRange.length; + const codeHeight = selectionBottom - selectionTop; + const previewEditorHeight = Math.max(codeHeight, editHeight); + + const clipped = dist === 0; + const codeEditDist = 0; + const previewEditorWidth = Math.min(previewContentWidth + MODIFIED_END_PADDING, remainingWidthRightOfEditor + editorLayout.width - editorLayout.contentLeft - codeEditDist); + + let editRect = Rect.fromLeftTopWidthHeight(codeRect.right + codeEditDist, selectionTop, previewEditorWidth, previewEditorHeight); + if (!isInsertion) { + editRect = editRect.withMargin(VERTICAL_PADDING, HORIZONTAL_PADDING).translateX(HORIZONTAL_PADDING + BORDER_WIDTH); + } else { + // Align top of edit with insertion line + editRect = editRect.withMargin(VERTICAL_PADDING, HORIZONTAL_PADDING).translateY(VERTICAL_PADDING); + } + + // debugView(debugLogRects({ codeRect, editRect }, this._editor.getDomNode()!), reader); + + return { + codeRect, + editRect, + codeScrollLeft: horizontalScrollOffset, + contentLeft: editorLayout.contentLeft, + + isInsertion, + maxContentWidth, + shouldShowShadow: clipped, + desiredPreviewEditorScrollLeft, + previewEditorWidth, + }; + }); + this._stickyScrollController = StickyScrollController.get(this._editorObs.editor); + this._stickyScrollHeight = this._stickyScrollController ? observableFromEvent(this._stickyScrollController.onDidChangeStickyScrollHeight, () => this._stickyScrollController!.stickyScrollWidgetHeight) : constObservable(0); + this._shouldOverflow = derived(reader => { + if (!ENABLE_OVERFLOW) { + return false; + } + const range = this._edit.read(reader)?.originalLineRange; + if (!range) { + return false; + } + const stickyScrollHeight = this._stickyScrollHeight.read(reader); + const top = this._editor.getTopForLineNumber(range.startLineNumber) - this._editorObs.scrollTop.read(reader); + if (top <= stickyScrollHeight) { + return false; + } + const bottom = this._editor.getTopForLineNumber(range.endLineNumberExclusive) - this._editorObs.scrollTop.read(reader); + if (bottom >= this._editorObs.layoutInfo.read(reader).height) { + return false; + } + return true; + }); + this._originalBackgroundColor = observableFromEvent(this, this._themeService.onDidColorThemeChange, () => { + return this._themeService.getColorTheme().getColor(originalBackgroundColor) ?? Color.transparent; + }); + this._backgroundSvg = n.svg({ + transform: 'translate(-0.5 -0.5)', + style: { overflow: 'visible', pointerEvents: 'none', position: 'absolute' }, + }, [ + n.svgElem('path', { + class: 'rightOfModifiedBackgroundCoverUp', + d: derived(reader => { + const layoutInfo = this._previewEditorLayoutInfo.read(reader); + if (!layoutInfo) { + return undefined; + } + const originalBackgroundColor = this._originalBackgroundColor.read(reader); + if (originalBackgroundColor.isTransparent()) { + return undefined; + } + + return new PathBuilder() + .moveTo(layoutInfo.codeRect.getRightTop()) + .lineTo(layoutInfo.codeRect.getRightTop().deltaX(1000)) + .lineTo(layoutInfo.codeRect.getRightBottom().deltaX(1000)) + .lineTo(layoutInfo.codeRect.getRightBottom()) + .build(); + }), + style: { + fill: asCssVariableWithDefault(editorBackground, 'transparent'), + } + }), + ]).keepUpdated(this._store); + this._originalOverlay = n.div({ + style: { pointerEvents: 'none', display: this._previewEditorLayoutInfo.map(layoutInfo => layoutInfo?.isInsertion ? 'none' : 'block') }, + }, derived(reader => { + const layoutInfoObs = mapOutFalsy(this._previewEditorLayoutInfo).read(reader); + if (!layoutInfoObs) { return undefined; } + + const borderStyling = getOriginalBorderColor(this._tabAction).map(bc => `${BORDER_WIDTH}px solid ${asCssVariable(bc)}`); + const borderStylingSeparator = `${BORDER_WIDTH + WIDGET_SEPARATOR_WIDTH}px solid ${asCssVariable(editorBackground)}`; + + const hasBorderLeft = layoutInfoObs.read(reader).codeScrollLeft !== 0; + const isModifiedLower = layoutInfoObs.map(layoutInfo => layoutInfo.codeRect.bottom < layoutInfo.editRect.bottom); + const transitionRectSize = BORDER_RADIUS * 2 + BORDER_WIDTH * 2; + + // Create an overlay which hides the left hand side of the original overlay when it overflows to the left + // such that there is a smooth transition at the edge of content left + const overlayHider = layoutInfoObs.map(layoutInfo => Rect.fromLeftTopRightBottom( + layoutInfo.contentLeft - BORDER_RADIUS - BORDER_WIDTH, + layoutInfo.codeRect.top, + layoutInfo.contentLeft, + layoutInfo.codeRect.bottom + transitionRectSize + )).read(reader); + + const intersectionLine = new OffsetRange(overlayHider.left, Number.MAX_SAFE_INTEGER); + const overlayRect = layoutInfoObs.map(layoutInfo => layoutInfo.codeRect.intersectHorizontal(intersectionLine)); + const separatorRect = overlayRect.map(overlayRect => overlayRect.withMargin(WIDGET_SEPARATOR_WIDTH, 0, WIDGET_SEPARATOR_WIDTH, WIDGET_SEPARATOR_WIDTH).intersectHorizontal(intersectionLine)); + + const transitionRect = overlayRect.map(overlayRect => Rect.fromLeftTopWidthHeight(overlayRect.right - transitionRectSize + BORDER_WIDTH, overlayRect.bottom - BORDER_WIDTH, transitionRectSize, transitionRectSize).intersectHorizontal(intersectionLine)); + + return [ + n.div({ + class: 'originalSeparatorSideBySide', + style: { + ...separatorRect.read(reader).toStyles(), + boxSizing: 'border-box', + borderRadius: `${BORDER_RADIUS}px 0 0 ${BORDER_RADIUS}px`, + borderTop: borderStylingSeparator, + borderBottom: borderStylingSeparator, + borderLeft: hasBorderLeft ? 'none' : borderStylingSeparator, + } + }), + + n.div({ + class: 'originalOverlaySideBySide', + style: { + ...overlayRect.read(reader).toStyles(), + boxSizing: 'border-box', + borderRadius: `${BORDER_RADIUS}px 0 0 ${BORDER_RADIUS}px`, + borderTop: borderStyling, + borderBottom: borderStyling, + borderLeft: hasBorderLeft ? 'none' : borderStyling, + backgroundColor: asCssVariable(originalBackgroundColor), + } + }), + + n.div({ + class: 'originalCornerCutoutSideBySide', + style: { + pointerEvents: 'none', + display: isModifiedLower.map(isLower => isLower ? 'block' : 'none'), + ...transitionRect.read(reader).toStyles(), + } + }, [ + n.div({ + class: 'originalCornerCutoutBackground', + style: { + position: 'absolute', top: '0px', left: '0px', width: '100%', height: '100%', + backgroundColor: getEditorBlendedColor(originalBackgroundColor, this._themeService).map(c => c.toString()), + } + }), + n.div({ + class: 'originalCornerCutoutBorder', + style: { + position: 'absolute', top: '0px', left: '0px', width: '100%', height: '100%', + boxSizing: 'border-box', + borderTop: borderStyling, + borderRight: borderStyling, + borderRadius: `0 100% 0 0`, + backgroundColor: asCssVariable(editorBackground) + } + }) + ]), + n.div({ + class: 'originalOverlaySideBySideHider', + style: { + ...overlayHider.toStyles(), + backgroundColor: asCssVariable(editorBackground), + } + }), + ]; + })).keepUpdated(this._store); + this._modifiedOverlay = n.div({ + style: { pointerEvents: 'none', } + }, derived(reader => { + const layoutInfoObs = mapOutFalsy(this._previewEditorLayoutInfo).read(reader); + if (!layoutInfoObs) { return undefined; } + + const isModifiedLower = layoutInfoObs.map(layoutInfo => layoutInfo.codeRect.bottom < layoutInfo.editRect.bottom); + + const borderRadius = isModifiedLower.map(isLower => `0 ${BORDER_RADIUS}px ${BORDER_RADIUS}px ${isLower ? BORDER_RADIUS : 0}px`); + const borderStyling = getEditorBlendedColor(getModifiedBorderColor(this._tabAction), this._themeService).map(c => `1px solid ${c.toString()}`); + const borderStylingSeparator = `${BORDER_WIDTH + WIDGET_SEPARATOR_WIDTH}px solid ${asCssVariable(editorBackground)}`; + + const overlayRect = layoutInfoObs.map(layoutInfo => layoutInfo.editRect.withMargin(0, BORDER_WIDTH)); + const separatorRect = overlayRect.map(overlayRect => overlayRect.withMargin(WIDGET_SEPARATOR_WIDTH, WIDGET_SEPARATOR_WIDTH, WIDGET_SEPARATOR_WIDTH, 0)); + + const insertionRect = derived(reader => { + const overlay = overlayRect.read(reader); + const layoutinfo = layoutInfoObs.read(reader); + if (!layoutinfo.isInsertion || layoutinfo.contentLeft >= overlay.left) { + return Rect.fromLeftTopWidthHeight(overlay.left, overlay.top, 0, 0); + } + return new Rect(layoutinfo.contentLeft, overlay.top, overlay.left, overlay.top + BORDER_WIDTH * 2); + }); + + return [ + n.div({ + class: 'modifiedInsertionSideBySide', + style: { + ...insertionRect.read(reader).toStyles(), + backgroundColor: getModifiedBorderColor(this._tabAction).map(c => asCssVariable(c)), + } + }), + n.div({ + class: 'modifiedSeparatorSideBySide', + style: { + ...separatorRect.read(reader).toStyles(), + borderRadius, + borderTop: borderStylingSeparator, + borderBottom: borderStylingSeparator, + borderRight: borderStylingSeparator, + boxSizing: 'border-box', + } + }), + n.div({ + class: 'modifiedOverlaySideBySide', + style: { + ...overlayRect.read(reader).toStyles(), + borderRadius, + border: borderStyling, + boxSizing: 'border-box', + backgroundColor: asCssVariable(modifiedBackgroundColor), + } + }) + ]; + })).keepUpdated(this._store); + this._nonOverflowView = n.div({ + class: 'inline-edits-view', + style: { + position: 'absolute', + overflow: 'visible', + top: '0px', + left: '0px', + display: this._display, + }, + }, [ + this._backgroundSvg, + derived(this, reader => this._shouldOverflow.read(reader) ? [] : [this._editorContainer, this._originalOverlay, this._modifiedOverlay]), + ]).keepUpdated(this._store); this._register(this._editorObs.createOverlayWidget({ domNode: this._nonOverflowView.element, @@ -113,478 +570,49 @@ export class InlineEditsSideBySideView extends Disposable implements IInlineEdit this._updatePreviewEditor.recomputeInitiallyAndOnChange(this._store); } - private readonly _display = derived(this, reader => !!this._uiState.read(reader) ? 'block' : 'none'); + private readonly _display; - private readonly previewRef = n.ref(); + private readonly previewRef; - private readonly _editorContainer = n.div({ - class: ['editorContainer'], - style: { position: 'absolute', overflow: 'hidden', cursor: 'pointer' }, - onmousedown: e => { - e.preventDefault(); // This prevents that the editor loses focus - }, - onclick: (e) => { - this._onDidClick.fire(new StandardMouseEvent(getWindow(e), e)); - } - }, [ - n.div({ class: 'preview', style: { pointerEvents: 'none' }, ref: this.previewRef }), - ]).keepUpdated(this._store); + private readonly _editorContainer; - public readonly isHovered = this._editorContainer.didMouseMoveDuringHover; + public readonly isHovered; - public readonly previewEditor = this._register(this._instantiationService.createInstance( - EmbeddedCodeEditorWidget, - this.previewRef.element, - { - glyphMargin: false, - lineNumbers: 'off', - minimap: { enabled: false }, - guides: { - indentation: false, - bracketPairs: false, - bracketPairsHorizontal: false, - highlightActiveIndentation: false, - }, - rulers: [], - padding: { top: 0, bottom: 0 }, - folding: false, - selectOnLineNumbers: false, - selectionHighlight: false, - columnSelection: false, - overviewRulerBorder: false, - overviewRulerLanes: 0, - lineDecorationsWidth: 0, - lineNumbersMinChars: 0, - revealHorizontalRightPadding: 0, - bracketPairColorization: { enabled: true, independentColorPoolPerBracketType: false }, - scrollBeyondLastLine: false, - scrollbar: { - vertical: 'hidden', - horizontal: 'hidden', - handleMouseWheel: false, - }, - readOnly: true, - wordWrap: 'off', - wordWrapOverride1: 'off', - wordWrapOverride2: 'off', - }, - { - contextKeyValues: { - [InlineCompletionContextKeys.inInlineEditsPreviewEditor.key]: true, - }, - contributions: [], - }, - this._editor - )); + public readonly previewEditor; - private readonly _previewEditorObs = observableCodeEditor(this.previewEditor); + private readonly _previewEditorObs; - private _activeViewZones: string[] = []; - private readonly _updatePreviewEditor = derived(reader => { - this._editorContainer.readEffect(reader); - this._previewEditorObs.model.read(reader); // update when the model is set + private _activeViewZones: string[]; + private readonly _updatePreviewEditor; - // Setting this here explicitly to make sure that the preview editor is - // visible when needed, we're also checking that these fields are defined - // because of the auto run initial - // Before removing these, verify with a non-monospace font family - this._display.read(reader); - if (this._nonOverflowView) { - this._nonOverflowView.element.style.display = this._display.read(reader); - } + private readonly _previewEditorWidth; - const uiState = this._uiState.read(reader); - const edit = this._edit.read(reader); - if (!uiState || !edit) { - return; - } + private readonly _cursorPosIfTouchesEdit; - const range = edit.originalLineRange; + private readonly _originalStartPosition; - const hiddenAreas: Range[] = []; - if (range.startLineNumber > 1) { - hiddenAreas.push(new Range(1, 1, range.startLineNumber - 1, 1)); - } - if (range.startLineNumber + uiState.newTextLineCount < this._previewTextModel.getLineCount() + 1) { - hiddenAreas.push(new Range(range.startLineNumber + uiState.newTextLineCount, 1, this._previewTextModel.getLineCount() + 1, 1)); - } + private readonly _originalEndPosition; - this.previewEditor.setHiddenAreas(hiddenAreas, undefined, true); + private readonly _originalVerticalStartPosition; + private readonly _originalVerticalEndPosition; - // TODO: is this the proper way to handle viewzones? - const previousViewZones = [...this._activeViewZones]; - this._activeViewZones = []; + private readonly _originalDisplayRange; + private readonly _editorMaxContentWidthInRange; - const reducedLinesCount = (range.endLineNumberExclusive - range.startLineNumber) - uiState.newTextLineCount; - this.previewEditor.changeViewZones((changeAccessor) => { - previousViewZones.forEach(id => changeAccessor.removeZone(id)); + private readonly _previewEditorLayoutInfo; - if (reducedLinesCount > 0) { - this._activeViewZones.push(changeAccessor.addZone({ - afterLineNumber: range.startLineNumber + uiState.newTextLineCount - 1, - heightInLines: reducedLinesCount, - showInHiddenAreas: true, - domNode: $('div.diagonal-fill.inline-edits-view-zone'), - })); - } - }); - }); + private _stickyScrollController; + private readonly _stickyScrollHeight; - private readonly _previewEditorWidth = derived(this, reader => { - const edit = this._edit.read(reader); - if (!edit) { return 0; } - this._updatePreviewEditor.read(reader); + private readonly _shouldOverflow; - return maxContentWidthInRange(this._previewEditorObs, edit.modifiedLineRange, reader); - }); + private readonly _originalBackgroundColor; - private readonly _cursorPosIfTouchesEdit = derived(this, reader => { - const cursorPos = this._editorObs.cursorPosition.read(reader); - const edit = this._edit.read(reader); - if (!edit || !cursorPos) { return undefined; } - return edit.modifiedLineRange.contains(cursorPos.lineNumber) ? cursorPos : undefined; - }); + private readonly _backgroundSvg; - private readonly _originalStartPosition = derived(this, (reader) => { - const inlineEdit = this._edit.read(reader); - return inlineEdit ? new Position(inlineEdit.originalLineRange.startLineNumber, 1) : null; - }); + private readonly _originalOverlay; - private readonly _originalEndPosition = derived(this, (reader) => { - const inlineEdit = this._edit.read(reader); - return inlineEdit ? new Position(inlineEdit.originalLineRange.endLineNumberExclusive, 1) : null; - }); + private readonly _modifiedOverlay; - private readonly _originalVerticalStartPosition = this._editorObs.observePosition(this._originalStartPosition, this._store).map(p => p?.y); - private readonly _originalVerticalEndPosition = this._editorObs.observePosition(this._originalEndPosition, this._store).map(p => p?.y); - - private readonly _originalDisplayRange = this._edit.map(e => e?.displayRange); - private readonly _editorMaxContentWidthInRange = derived(this, reader => { - const originalDisplayRange = this._originalDisplayRange.read(reader); - if (!originalDisplayRange) { - return constObservable(0); - } - this._editorObs.versionId.read(reader); - - // Take the max value that we observed. - // Reset when either the edit changes or the editor text version. - return derivedObservableWithCache(this, (reader, lastValue) => { - const maxWidth = maxContentWidthInRange(this._editorObs, originalDisplayRange, reader); - return Math.max(maxWidth, lastValue ?? 0); - }); - }).map((v, r) => v.read(r)); - - private readonly _previewEditorLayoutInfo = derived(this, (reader) => { - const inlineEdit = this._edit.read(reader); - if (!inlineEdit) { - return null; - } - const state = this._uiState.read(reader); - if (!state) { - return null; - } - - const range = inlineEdit.originalLineRange; - - const horizontalScrollOffset = this._editorObs.scrollLeft.read(reader); - - const editorContentMaxWidthInRange = this._editorMaxContentWidthInRange.read(reader); - const editorLayout = this._editorObs.layoutInfo.read(reader); - const previewContentWidth = this._previewEditorWidth.read(reader); - const editorContentAreaWidth = editorLayout.contentWidth - editorLayout.verticalScrollbarWidth; - const editorBoundingClientRect = this._editor.getContainerDomNode().getBoundingClientRect(); - const clientContentAreaRight = editorLayout.contentLeft + editorLayout.contentWidth + editorBoundingClientRect.left; - const remainingWidthRightOfContent = getWindow(this._editor.getContainerDomNode()).innerWidth - clientContentAreaRight; - const remainingWidthRightOfEditor = getWindow(this._editor.getContainerDomNode()).innerWidth - editorBoundingClientRect.right; - const desiredMinimumWidth = Math.min(editorLayout.contentWidth * 0.3, previewContentWidth, 100); - const IN_EDITOR_DISPLACEMENT = 0; - const maximumAvailableWidth = IN_EDITOR_DISPLACEMENT + remainingWidthRightOfContent; - - const cursorPos = this._cursorPosIfTouchesEdit.read(reader); - - const maxPreviewEditorLeft = Math.max( - // We're starting from the content area right and moving it left by IN_EDITOR_DISPLACEMENT and also by an amount to ensure some minimum desired width - editorContentAreaWidth + horizontalScrollOffset - IN_EDITOR_DISPLACEMENT - Math.max(0, desiredMinimumWidth - maximumAvailableWidth), - // But we don't want that the moving left ends up covering the cursor, so this will push it to the right again - Math.min( - cursorPos ? getOffsetForPos(this._editorObs, cursorPos, reader) + 50 : 0, - editorContentAreaWidth + horizontalScrollOffset - ) - ); - const previewEditorLeftInTextArea = Math.min(editorContentMaxWidthInRange + ORIGINAL_END_PADDING, maxPreviewEditorLeft); - - const maxContentWidth = editorContentMaxWidthInRange + ORIGINAL_END_PADDING + previewContentWidth + 70; - - const dist = maxPreviewEditorLeft - previewEditorLeftInTextArea; - - let desiredPreviewEditorScrollLeft; - let codeRight; - if (previewEditorLeftInTextArea > horizontalScrollOffset) { - desiredPreviewEditorScrollLeft = 0; - codeRight = editorLayout.contentLeft + previewEditorLeftInTextArea - horizontalScrollOffset; - } else { - desiredPreviewEditorScrollLeft = horizontalScrollOffset - previewEditorLeftInTextArea; - codeRight = editorLayout.contentLeft; - } - - const selectionTop = this._originalVerticalStartPosition.read(reader) ?? this._editor.getTopForLineNumber(range.startLineNumber) - this._editorObs.scrollTop.read(reader); - const selectionBottom = this._originalVerticalEndPosition.read(reader) ?? this._editor.getBottomForLineNumber(range.endLineNumberExclusive - 1) - this._editorObs.scrollTop.read(reader); - - // TODO: const { prefixLeftOffset } = getPrefixTrim(inlineEdit.edit.edits.map(e => e.range), inlineEdit.originalLineRange, [], this._editor); - const codeLeft = editorLayout.contentLeft - horizontalScrollOffset; - - let codeRect = Rect.fromLeftTopRightBottom(codeLeft, selectionTop, codeRight, selectionBottom); - const isInsertion = codeRect.height === 0; - if (!isInsertion) { - codeRect = codeRect.withMargin(VERTICAL_PADDING, HORIZONTAL_PADDING); - } - - const editHeight = this._editor.getOption(EditorOption.lineHeight) * inlineEdit.modifiedLineRange.length; - const codeHeight = selectionBottom - selectionTop; - const previewEditorHeight = Math.max(codeHeight, editHeight); - - const clipped = dist === 0; - const codeEditDist = 0; - const previewEditorWidth = Math.min(previewContentWidth + MODIFIED_END_PADDING, remainingWidthRightOfEditor + editorLayout.width - editorLayout.contentLeft - codeEditDist); - - let editRect = Rect.fromLeftTopWidthHeight(codeRect.right + codeEditDist, selectionTop, previewEditorWidth, previewEditorHeight); - if (!isInsertion) { - editRect = editRect.withMargin(VERTICAL_PADDING, HORIZONTAL_PADDING).translateX(HORIZONTAL_PADDING + BORDER_WIDTH); - } else { - // Align top of edit with insertion line - editRect = editRect.withMargin(VERTICAL_PADDING, HORIZONTAL_PADDING).translateY(VERTICAL_PADDING); - } - - // debugView(debugLogRects({ codeRect, editRect }, this._editor.getDomNode()!), reader); - - return { - codeRect, - editRect, - codeScrollLeft: horizontalScrollOffset, - contentLeft: editorLayout.contentLeft, - - isInsertion, - maxContentWidth, - shouldShowShadow: clipped, - desiredPreviewEditorScrollLeft, - previewEditorWidth, - }; - }); - - private _stickyScrollController = StickyScrollController.get(this._editorObs.editor); - private readonly _stickyScrollHeight = this._stickyScrollController ? observableFromEvent(this._stickyScrollController.onDidChangeStickyScrollHeight, () => this._stickyScrollController!.stickyScrollWidgetHeight) : constObservable(0); - - private readonly _shouldOverflow = derived(reader => { - if (!ENABLE_OVERFLOW) { - return false; - } - const range = this._edit.read(reader)?.originalLineRange; - if (!range) { - return false; - } - const stickyScrollHeight = this._stickyScrollHeight.read(reader); - const top = this._editor.getTopForLineNumber(range.startLineNumber) - this._editorObs.scrollTop.read(reader); - if (top <= stickyScrollHeight) { - return false; - } - const bottom = this._editor.getTopForLineNumber(range.endLineNumberExclusive) - this._editorObs.scrollTop.read(reader); - if (bottom >= this._editorObs.layoutInfo.read(reader).height) { - return false; - } - return true; - }); - - private readonly _originalBackgroundColor = observableFromEvent(this, this._themeService.onDidColorThemeChange, () => { - return this._themeService.getColorTheme().getColor(originalBackgroundColor) ?? Color.transparent; - }); - - private readonly _backgroundSvg = n.svg({ - transform: 'translate(-0.5 -0.5)', - style: { overflow: 'visible', pointerEvents: 'none', position: 'absolute' }, - }, [ - n.svgElem('path', { - class: 'rightOfModifiedBackgroundCoverUp', - d: derived(reader => { - const layoutInfo = this._previewEditorLayoutInfo.read(reader); - if (!layoutInfo) { - return undefined; - } - const originalBackgroundColor = this._originalBackgroundColor.read(reader); - if (originalBackgroundColor.isTransparent()) { - return undefined; - } - - return new PathBuilder() - .moveTo(layoutInfo.codeRect.getRightTop()) - .lineTo(layoutInfo.codeRect.getRightTop().deltaX(1000)) - .lineTo(layoutInfo.codeRect.getRightBottom().deltaX(1000)) - .lineTo(layoutInfo.codeRect.getRightBottom()) - .build(); - }), - style: { - fill: asCssVariableWithDefault(editorBackground, 'transparent'), - } - }), - ]).keepUpdated(this._store); - - private readonly _originalOverlay = n.div({ - style: { pointerEvents: 'none', display: this._previewEditorLayoutInfo.map(layoutInfo => layoutInfo?.isInsertion ? 'none' : 'block') }, - }, derived(reader => { - const layoutInfoObs = mapOutFalsy(this._previewEditorLayoutInfo).read(reader); - if (!layoutInfoObs) { return undefined; } - - const borderStyling = getOriginalBorderColor(this._tabAction).map(bc => `${BORDER_WIDTH}px solid ${asCssVariable(bc)}`); - const borderStylingSeparator = `${BORDER_WIDTH + WIDGET_SEPARATOR_WIDTH}px solid ${asCssVariable(editorBackground)}`; - - const hasBorderLeft = layoutInfoObs.read(reader).codeScrollLeft !== 0; - const isModifiedLower = layoutInfoObs.map(layoutInfo => layoutInfo.codeRect.bottom < layoutInfo.editRect.bottom); - const transitionRectSize = BORDER_RADIUS * 2 + BORDER_WIDTH * 2; - - // Create an overlay which hides the left hand side of the original overlay when it overflows to the left - // such that there is a smooth transition at the edge of content left - const overlayHider = layoutInfoObs.map(layoutInfo => Rect.fromLeftTopRightBottom( - layoutInfo.contentLeft - BORDER_RADIUS - BORDER_WIDTH, - layoutInfo.codeRect.top, - layoutInfo.contentLeft, - layoutInfo.codeRect.bottom + transitionRectSize - )).read(reader); - - const intersectionLine = new OffsetRange(overlayHider.left, Number.MAX_SAFE_INTEGER); - const overlayRect = layoutInfoObs.map(layoutInfo => layoutInfo.codeRect.intersectHorizontal(intersectionLine)); - const separatorRect = overlayRect.map(overlayRect => overlayRect.withMargin(WIDGET_SEPARATOR_WIDTH, 0, WIDGET_SEPARATOR_WIDTH, WIDGET_SEPARATOR_WIDTH).intersectHorizontal(intersectionLine)); - - const transitionRect = overlayRect.map(overlayRect => Rect.fromLeftTopWidthHeight(overlayRect.right - transitionRectSize + BORDER_WIDTH, overlayRect.bottom - BORDER_WIDTH, transitionRectSize, transitionRectSize).intersectHorizontal(intersectionLine)); - - return [ - n.div({ - class: 'originalSeparatorSideBySide', - style: { - ...separatorRect.read(reader).toStyles(), - boxSizing: 'border-box', - borderRadius: `${BORDER_RADIUS}px 0 0 ${BORDER_RADIUS}px`, - borderTop: borderStylingSeparator, - borderBottom: borderStylingSeparator, - borderLeft: hasBorderLeft ? 'none' : borderStylingSeparator, - } - }), - - n.div({ - class: 'originalOverlaySideBySide', - style: { - ...overlayRect.read(reader).toStyles(), - boxSizing: 'border-box', - borderRadius: `${BORDER_RADIUS}px 0 0 ${BORDER_RADIUS}px`, - borderTop: borderStyling, - borderBottom: borderStyling, - borderLeft: hasBorderLeft ? 'none' : borderStyling, - backgroundColor: asCssVariable(originalBackgroundColor), - } - }), - - n.div({ - class: 'originalCornerCutoutSideBySide', - style: { - pointerEvents: 'none', - display: isModifiedLower.map(isLower => isLower ? 'block' : 'none'), - ...transitionRect.read(reader).toStyles(), - } - }, [ - n.div({ - class: 'originalCornerCutoutBackground', - style: { - position: 'absolute', top: '0px', left: '0px', width: '100%', height: '100%', - backgroundColor: getEditorBlendedColor(originalBackgroundColor, this._themeService).map(c => c.toString()), - } - }), - n.div({ - class: 'originalCornerCutoutBorder', - style: { - position: 'absolute', top: '0px', left: '0px', width: '100%', height: '100%', - boxSizing: 'border-box', - borderTop: borderStyling, - borderRight: borderStyling, - borderRadius: `0 100% 0 0`, - backgroundColor: asCssVariable(editorBackground) - } - }) - ]), - n.div({ - class: 'originalOverlaySideBySideHider', - style: { - ...overlayHider.toStyles(), - backgroundColor: asCssVariable(editorBackground), - } - }), - ]; - })).keepUpdated(this._store); - - private readonly _modifiedOverlay = n.div({ - style: { pointerEvents: 'none', } - }, derived(reader => { - const layoutInfoObs = mapOutFalsy(this._previewEditorLayoutInfo).read(reader); - if (!layoutInfoObs) { return undefined; } - - const isModifiedLower = layoutInfoObs.map(layoutInfo => layoutInfo.codeRect.bottom < layoutInfo.editRect.bottom); - - const borderRadius = isModifiedLower.map(isLower => `0 ${BORDER_RADIUS}px ${BORDER_RADIUS}px ${isLower ? BORDER_RADIUS : 0}px`); - const borderStyling = getEditorBlendedColor(getModifiedBorderColor(this._tabAction), this._themeService).map(c => `1px solid ${c.toString()}`); - const borderStylingSeparator = `${BORDER_WIDTH + WIDGET_SEPARATOR_WIDTH}px solid ${asCssVariable(editorBackground)}`; - - const overlayRect = layoutInfoObs.map(layoutInfo => layoutInfo.editRect.withMargin(0, BORDER_WIDTH)); - const separatorRect = overlayRect.map(overlayRect => overlayRect.withMargin(WIDGET_SEPARATOR_WIDTH, WIDGET_SEPARATOR_WIDTH, WIDGET_SEPARATOR_WIDTH, 0)); - - const insertionRect = derived(reader => { - const overlay = overlayRect.read(reader); - const layoutinfo = layoutInfoObs.read(reader); - if (!layoutinfo.isInsertion || layoutinfo.contentLeft >= overlay.left) { - return Rect.fromLeftTopWidthHeight(overlay.left, overlay.top, 0, 0); - } - return new Rect(layoutinfo.contentLeft, overlay.top, overlay.left, overlay.top + BORDER_WIDTH * 2); - }); - - return [ - n.div({ - class: 'modifiedInsertionSideBySide', - style: { - ...insertionRect.read(reader).toStyles(), - backgroundColor: getModifiedBorderColor(this._tabAction).map(c => asCssVariable(c)), - } - }), - n.div({ - class: 'modifiedSeparatorSideBySide', - style: { - ...separatorRect.read(reader).toStyles(), - borderRadius, - borderTop: borderStylingSeparator, - borderBottom: borderStylingSeparator, - borderRight: borderStylingSeparator, - boxSizing: 'border-box', - } - }), - n.div({ - class: 'modifiedOverlaySideBySide', - style: { - ...overlayRect.read(reader).toStyles(), - borderRadius, - border: borderStyling, - boxSizing: 'border-box', - backgroundColor: asCssVariable(modifiedBackgroundColor), - } - }) - ]; - })).keepUpdated(this._store); - - private readonly _nonOverflowView = n.div({ - class: 'inline-edits-view', - style: { - position: 'absolute', - overflow: 'visible', - top: '0px', - left: '0px', - display: this._display, - }, - }, [ - this._backgroundSvg, - derived(this, reader => this._shouldOverflow.read(reader) ? [] : [this._editorContainer, this._originalOverlay, this._modifiedOverlay]), - ]).keepUpdated(this._store); + private readonly _nonOverflowView; } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsWordInsertView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsWordInsertView.ts index ac60eb381de..f30d563df5c 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsWordInsertView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsWordInsertView.ts @@ -13,118 +13,124 @@ import { ObservableCodeEditor } from '../../../../../../browser/observableCodeEd import { Point } from '../../../../../../common/core/2d/point.js'; import { Rect } from '../../../../../../common/core/2d/rect.js'; import { EditorOption } from '../../../../../../common/config/editorOptions.js'; -import { OffsetRange } from '../../../../../../common/core/offsetRange.js'; -import { SingleTextEdit } from '../../../../../../common/core/edits/textEdit.js'; +import { OffsetRange } from '../../../../../../common/core/ranges/offsetRange.js'; +import { TextReplacement } from '../../../../../../common/core/edits/textEdit.js'; import { IInlineEditsView, InlineEditTabAction } from '../inlineEditsViewInterface.js'; import { getModifiedBorderColor } from '../theme.js'; import { mapOutFalsy, rectToProps } from '../utils/utils.js'; export class InlineEditsWordInsertView extends Disposable implements IInlineEditsView { - private readonly _onDidClick = this._register(new Emitter()); - readonly onDidClick = this._onDidClick.event; + private readonly _onDidClick; + readonly onDidClick; - private readonly _start = this._editor.observePosition(constObservable(this._edit.range.getStartPosition()), this._store); + private readonly _start; - private readonly _layout = derived(this, reader => { - const start = this._start.read(reader); - if (!start) { - return undefined; - } - const contentLeft = this._editor.layoutInfoContentLeft.read(reader); - const lineHeight = this._editor.getOption(EditorOption.lineHeight).read(reader); + private readonly _layout; - const w = this._editor.getOption(EditorOption.fontInfo).read(reader).typicalHalfwidthCharacterWidth; - const width = this._edit.text.length * w + 5; + private readonly _div; - const center = new Point(contentLeft + start.x + w / 2 - this._editor.scrollLeft.read(reader), start.y); - - const modified = Rect.fromLeftTopWidthHeight(center.x - width / 2, center.y + lineHeight + 5, width, lineHeight); - const background = Rect.hull([Rect.fromPoint(center), modified]).withMargin(4); - - return { - modified, - center, - background, - lowerBackground: background.intersectVertical(new OffsetRange(modified.top - 2, Number.MAX_SAFE_INTEGER)), - }; - }); - - private readonly _div = n.div({ - class: 'word-insert', - }, [ - derived(reader => { - const layout = mapOutFalsy(this._layout).read(reader); - if (!layout) { - return []; - } - - const modifiedBorderColor = asCssVariable(getModifiedBorderColor(this._tabAction).read(reader)); - - return [ - n.div({ - style: { - position: 'absolute', - ...rectToProps(reader => layout.read(reader).lowerBackground), - borderRadius: '4px', - background: 'var(--vscode-editor-background)' - } - }, []), - n.div({ - style: { - position: 'absolute', - ...rectToProps(reader => layout.read(reader).modified), - borderRadius: '4px', - padding: '0px', - textAlign: 'center', - background: 'var(--vscode-inlineEdit-modifiedChangedTextBackground)', - fontFamily: this._editor.getOption(EditorOption.fontFamily), - fontSize: this._editor.getOption(EditorOption.fontSize), - fontWeight: this._editor.getOption(EditorOption.fontWeight), - } - }, [ - this._edit.text, - ]), - n.div({ - style: { - position: 'absolute', - ...rectToProps(reader => layout.read(reader).background), - borderRadius: '4px', - border: `1px solid ${modifiedBorderColor}`, - //background: 'rgba(122, 122, 122, 0.12)', looks better - background: 'var(--vscode-inlineEdit-wordReplacementView-background)', - } - }, []), - n.svg({ - viewBox: '0 0 12 18', - width: 12, - height: 18, - fill: 'none', - style: { - position: 'absolute', - left: derived(reader => layout.read(reader).center.x - 9), - top: derived(reader => layout.read(reader).center.y + 4), - transform: 'scale(1.4, 1.4)', - } - }, [ - n.svgElem('path', { - d: 'M5.06445 0H7.35759C7.35759 0 7.35759 8.47059 7.35759 11.1176C7.35759 13.7647 9.4552 18 13.4674 18C17.4795 18 -2.58445 18 0.281373 18C3.14719 18 5.06477 14.2941 5.06477 11.1176C5.06477 7.94118 5.06445 0 5.06445 0Z', - fill: 'var(--vscode-inlineEdit-modifiedChangedTextBackground)', - }) - ]) - ]; - }) - ]).keepUpdated(this._store); - - readonly isHovered = constObservable(false); + readonly isHovered; constructor( private readonly _editor: ObservableCodeEditor, /** Must be single-line in both sides */ - private readonly _edit: SingleTextEdit, + private readonly _edit: TextReplacement, private readonly _tabAction: IObservable ) { super(); + this._onDidClick = this._register(new Emitter()); + this.onDidClick = this._onDidClick.event; + this._start = this._editor.observePosition(constObservable(this._edit.range.getStartPosition()), this._store); + this._layout = derived(this, reader => { + const start = this._start.read(reader); + if (!start) { + return undefined; + } + const contentLeft = this._editor.layoutInfoContentLeft.read(reader); + const lineHeight = this._editor.getOption(EditorOption.lineHeight).read(reader); + + const w = this._editor.getOption(EditorOption.fontInfo).read(reader).typicalHalfwidthCharacterWidth; + const width = this._edit.text.length * w + 5; + + const center = new Point(contentLeft + start.x + w / 2 - this._editor.scrollLeft.read(reader), start.y); + + const modified = Rect.fromLeftTopWidthHeight(center.x - width / 2, center.y + lineHeight + 5, width, lineHeight); + const background = Rect.hull([Rect.fromPoint(center), modified]).withMargin(4); + + return { + modified, + center, + background, + lowerBackground: background.intersectVertical(new OffsetRange(modified.top - 2, Number.MAX_SAFE_INTEGER)), + }; + }); + this._div = n.div({ + class: 'word-insert', + }, [ + derived(reader => { + const layout = mapOutFalsy(this._layout).read(reader); + if (!layout) { + return []; + } + + const modifiedBorderColor = asCssVariable(getModifiedBorderColor(this._tabAction).read(reader)); + + return [ + n.div({ + style: { + position: 'absolute', + ...rectToProps(reader => layout.read(reader).lowerBackground), + borderRadius: '4px', + background: 'var(--vscode-editor-background)' + } + }, []), + n.div({ + style: { + position: 'absolute', + ...rectToProps(reader => layout.read(reader).modified), + borderRadius: '4px', + padding: '0px', + textAlign: 'center', + background: 'var(--vscode-inlineEdit-modifiedChangedTextBackground)', + fontFamily: this._editor.getOption(EditorOption.fontFamily), + fontSize: this._editor.getOption(EditorOption.fontSize), + fontWeight: this._editor.getOption(EditorOption.fontWeight), + } + }, [ + this._edit.text, + ]), + n.div({ + style: { + position: 'absolute', + ...rectToProps(reader => layout.read(reader).background), + borderRadius: '4px', + border: `1px solid ${modifiedBorderColor}`, + //background: 'rgba(122, 122, 122, 0.12)', looks better + background: 'var(--vscode-inlineEdit-wordReplacementView-background)', + } + }, []), + n.svg({ + viewBox: '0 0 12 18', + width: 12, + height: 18, + fill: 'none', + style: { + position: 'absolute', + left: derived(reader => layout.read(reader).center.x - 9), + top: derived(reader => layout.read(reader).center.y + 4), + transform: 'scale(1.4, 1.4)', + } + }, [ + n.svgElem('path', { + d: 'M5.06445 0H7.35759C7.35759 0 7.35759 8.47059 7.35759 11.1176C7.35759 13.7647 9.4552 18 13.4674 18C17.4795 18 -2.58445 18 0.281373 18C3.14719 18 5.06477 14.2941 5.06477 11.1176C5.06477 7.94118 5.06445 0 5.06445 0Z', + fill: 'var(--vscode-inlineEdit-modifiedChangedTextBackground)', + }) + ]) + ]; + }) + ]).keepUpdated(this._store); + this.isHovered = constObservable(false); this._register(this._editor.createOverlayWidget({ domNode: this._div.element, diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsWordReplacementView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsWordReplacementView.ts index d9cc00e40c5..1ee9fab7b66 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsWordReplacementView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsWordReplacementView.ts @@ -15,9 +15,9 @@ import { Point } from '../../../../../../common/core/2d/point.js'; import { Rect } from '../../../../../../common/core/2d/rect.js'; import { LineSource, renderLines, RenderOptions } from '../../../../../../browser/widget/diffEditor/components/diffEditorViewZones/renderLines.js'; import { EditorOption } from '../../../../../../common/config/editorOptions.js'; -import { SingleOffsetEdit } from '../../../../../../common/core/edits/offsetEdit.js'; -import { OffsetRange } from '../../../../../../common/core/offsetRange.js'; -import { SingleTextEdit } from '../../../../../../common/core/edits/textEdit.js'; +import { StringReplacement } from '../../../../../../common/core/edits/stringEdit.js'; +import { OffsetRange } from '../../../../../../common/core/ranges/offsetRange.js'; +import { TextReplacement } from '../../../../../../common/core/edits/textEdit.js'; import { ILanguageService } from '../../../../../../common/languages/language.js'; import { LineTokens } from '../../../../../../common/tokens/lineTokens.js'; import { TokenArray } from '../../../../../../common/tokens/tokenArray.js'; @@ -29,26 +29,183 @@ export class InlineEditsWordReplacementView extends Disposable implements IInlin public static MAX_LENGTH = 100; - private readonly _onDidClick = this._register(new Emitter()); - readonly onDidClick = this._onDidClick.event; + private readonly _onDidClick; + readonly onDidClick; - private readonly _start = this._editor.observePosition(constObservable(this._edit.range.getStartPosition()), this._store); - private readonly _end = this._editor.observePosition(constObservable(this._edit.range.getEndPosition()), this._store); + private readonly _start; + private readonly _end; - private readonly _line = document.createElement('div'); + private readonly _line; - private readonly _hoverableElement = observableValue(this, null); + private readonly _hoverableElement; - readonly isHovered = this._hoverableElement.map((e, reader) => e?.didMouseMoveDuringHover.read(reader) ?? false); + readonly isHovered; constructor( private readonly _editor: ObservableCodeEditor, /** Must be single-line in both sides */ - private readonly _edit: SingleTextEdit, + private readonly _edit: TextReplacement, protected readonly _tabAction: IObservable, @ILanguageService private readonly _languageService: ILanguageService, ) { super(); + this._onDidClick = this._register(new Emitter()); + this.onDidClick = this._onDidClick.event; + this._start = this._editor.observePosition(constObservable(this._edit.range.getStartPosition()), this._store); + this._end = this._editor.observePosition(constObservable(this._edit.range.getEndPosition()), this._store); + this._line = document.createElement('div'); + this._hoverableElement = observableValue(this, null); + this.isHovered = this._hoverableElement.map((e, reader) => e?.didMouseMoveDuringHover.read(reader) ?? false); + this._renderTextEffect = derived(_reader => { + const tm = this._editor.model.get()!; + const origLine = tm.getLineContent(this._edit.range.startLineNumber); + + const edit = StringReplacement.replace(new OffsetRange(this._edit.range.startColumn - 1, this._edit.range.endColumn - 1), this._edit.text); + const lineToTokenize = edit.replace(origLine); + const t = tm.tokenization.tokenizeLinesAt(this._edit.range.startLineNumber, [lineToTokenize])?.[0]; + let tokens: LineTokens; + if (t) { + tokens = TokenArray.fromLineTokens(t).slice(edit.getRangeAfterReplace()).toLineTokens(this._edit.text, this._languageService.languageIdCodec); + } else { + tokens = LineTokens.createEmpty(this._edit.text, this._languageService.languageIdCodec); + } + const res = renderLines(new LineSource([tokens]), RenderOptions.fromEditor(this._editor.editor).withSetWidth(false).withScrollBeyondLastColumn(0), [], this._line, true); + this._line.style.width = `${res.minWidthInPx}px`; + }); + this._layout = derived(this, reader => { + this._renderTextEffect.read(reader); + const widgetStart = this._start.read(reader); + const widgetEnd = this._end.read(reader); + + // TODO@hediet better about widgetStart and widgetEnd in a single transaction! + if (!widgetStart || !widgetEnd || widgetStart.x > widgetEnd.x || widgetStart.y > widgetEnd.y) { + return undefined; + } + + const lineHeight = this._editor.getOption(EditorOption.lineHeight).read(reader); + const scrollLeft = this._editor.scrollLeft.read(reader); + const w = this._editor.getOption(EditorOption.fontInfo).read(reader).typicalHalfwidthCharacterWidth; + + const modifiedLeftOffset = 3 * w; + const modifiedTopOffset = 4; + const modifiedOffset = new Point(modifiedLeftOffset, modifiedTopOffset); + + const originalLine = Rect.fromPoints(widgetStart, widgetEnd).withHeight(lineHeight).translateX(-scrollLeft); + const modifiedLine = Rect.fromPointSize(originalLine.getLeftBottom().add(modifiedOffset), new Point(this._edit.text.length * w, originalLine.height)); + + const lowerBackground = modifiedLine.withLeft(originalLine.left); + + // debugView(debugLogRects({ lowerBackground }, this._editor.editor.getContainerDomNode()), reader); + + return { + originalLine, + modifiedLine, + lowerBackground, + lineHeight, + }; + }); + this._root = n.div({ + class: 'word-replacement', + }, [ + derived(reader => { + const layout = mapOutFalsy(this._layout).read(reader); + if (!layout) { + return []; + } + + const contentLeft = this._editor.layoutInfoContentLeft.read(reader); + const borderWidth = 1; + + const originalBorderColor = getOriginalBorderColor(this._tabAction).map(c => asCssVariable(c)).read(reader); + const modifiedBorderColor = getModifiedBorderColor(this._tabAction).map(c => asCssVariable(c)).read(reader); + + return [ + n.div({ + style: { + position: 'absolute', + top: 0, + left: contentLeft, + width: this._editor.contentWidth, + height: this._editor.editor.getContentHeight(), + overflow: 'hidden', + pointerEvents: 'none', + } + }, [ + n.div({ + style: { + position: 'absolute', + ...rectToProps(reader => layout.read(reader).lowerBackground.withMargin(borderWidth, 2 * borderWidth, borderWidth, 0)), + background: asCssVariable(editorBackground), + //boxShadow: `${asCssVariable(scrollbarShadow)} 0 6px 6px -6px`, + cursor: 'pointer', + pointerEvents: 'auto', + }, + onmousedown: e => { + e.preventDefault(); // This prevents that the editor loses focus + }, + onmouseup: (e) => this._onDidClick.fire(new StandardMouseEvent(getWindow(e), e)), + obsRef: (elem) => { + this._hoverableElement.set(elem, undefined); + } + }), + n.div({ + style: { + position: 'absolute', + ...rectToProps(reader => layout.read(reader).modifiedLine.withMargin(1, 2)), + fontFamily: this._editor.getOption(EditorOption.fontFamily), + fontSize: this._editor.getOption(EditorOption.fontSize), + fontWeight: this._editor.getOption(EditorOption.fontWeight), + + pointerEvents: 'none', + boxSizing: 'border-box', + borderRadius: '4px', + border: `${borderWidth}px solid ${modifiedBorderColor}`, + + background: asCssVariable(modifiedChangedTextOverlayColor), + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + + outline: `2px solid ${asCssVariable(editorBackground)}`, + } + }, [this._line]), + n.div({ + style: { + position: 'absolute', + ...rectToProps(reader => layout.read(reader).originalLine.withMargin(1)), + boxSizing: 'border-box', + borderRadius: '4px', + border: `${borderWidth}px solid ${originalBorderColor}`, + background: asCssVariable(originalChangedTextOverlayColor), + pointerEvents: 'none', + } + }, []), + + n.svg({ + width: 11, + height: 14, + viewBox: '0 0 11 14', + fill: 'none', + style: { + position: 'absolute', + left: layout.map(l => l.modifiedLine.left - 16), + top: layout.map(l => l.modifiedLine.top + Math.round((l.lineHeight - 14 - 5) / 2)), + } + }, [ + n.svgElem('path', { + d: 'M1 0C1 2.98966 1 5.92087 1 8.49952C1 9.60409 1.89543 10.5 3 10.5H10.5', + stroke: asCssVariable(editorHoverForeground), + }), + n.svgElem('path', { + d: 'M6 7.5L9.99999 10.49998L6 13.5', + stroke: asCssVariable(editorHoverForeground), + }) + ]), + + ]) + ]; + }) + ]).keepUpdated(this._store); this._register(this._editor.createOverlayWidget({ domNode: this._root.element, @@ -58,156 +215,9 @@ export class InlineEditsWordReplacementView extends Disposable implements IInlin })); } - private readonly _renderTextEffect = derived(_reader => { - const tm = this._editor.model.get()!; - const origLine = tm.getLineContent(this._edit.range.startLineNumber); + private readonly _renderTextEffect; - const edit = SingleOffsetEdit.replace(new OffsetRange(this._edit.range.startColumn - 1, this._edit.range.endColumn - 1), this._edit.text); - const lineToTokenize = edit.apply(origLine); - const t = tm.tokenization.tokenizeLinesAt(this._edit.range.startLineNumber, [lineToTokenize])?.[0]; - let tokens: LineTokens; - if (t) { - tokens = TokenArray.fromLineTokens(t).slice(edit.getRangeAfterApply()).toLineTokens(this._edit.text, this._languageService.languageIdCodec); - } else { - tokens = LineTokens.createEmpty(this._edit.text, this._languageService.languageIdCodec); - } - const res = renderLines(new LineSource([tokens]), RenderOptions.fromEditor(this._editor.editor).withSetWidth(false).withScrollBeyondLastColumn(0), [], this._line, true); - this._line.style.width = `${res.minWidthInPx}px`; - }); + private readonly _layout; - private readonly _layout = derived(this, reader => { - this._renderTextEffect.read(reader); - const widgetStart = this._start.read(reader); - const widgetEnd = this._end.read(reader); - - // TODO@hediet better about widgetStart and widgetEnd in a single transaction! - if (!widgetStart || !widgetEnd || widgetStart.x > widgetEnd.x || widgetStart.y > widgetEnd.y) { - return undefined; - } - - const lineHeight = this._editor.getOption(EditorOption.lineHeight).read(reader); - const scrollLeft = this._editor.scrollLeft.read(reader); - const w = this._editor.getOption(EditorOption.fontInfo).read(reader).typicalHalfwidthCharacterWidth; - - const modifiedLeftOffset = 3 * w; - const modifiedTopOffset = 4; - const modifiedOffset = new Point(modifiedLeftOffset, modifiedTopOffset); - - const originalLine = Rect.fromPoints(widgetStart, widgetEnd).withHeight(lineHeight).translateX(-scrollLeft); - const modifiedLine = Rect.fromPointSize(originalLine.getLeftBottom().add(modifiedOffset), new Point(this._edit.text.length * w, originalLine.height)); - - const lowerBackground = modifiedLine.withLeft(originalLine.left); - - // debugView(debugLogRects({ lowerBackground }, this._editor.editor.getContainerDomNode()), reader); - - return { - originalLine, - modifiedLine, - lowerBackground, - lineHeight, - }; - }); - - private readonly _root = n.div({ - class: 'word-replacement', - }, [ - derived(reader => { - const layout = mapOutFalsy(this._layout).read(reader); - if (!layout) { - return []; - } - - const contentLeft = this._editor.layoutInfoContentLeft.read(reader); - const borderWidth = 1; - - const originalBorderColor = getOriginalBorderColor(this._tabAction).map(c => asCssVariable(c)).read(reader); - const modifiedBorderColor = getModifiedBorderColor(this._tabAction).map(c => asCssVariable(c)).read(reader); - - return [ - n.div({ - style: { - position: 'absolute', - top: 0, - left: contentLeft, - width: this._editor.contentWidth, - height: this._editor.editor.getContentHeight(), - overflow: 'hidden', - pointerEvents: 'none', - } - }, [ - n.div({ - style: { - position: 'absolute', - ...rectToProps(reader => layout.read(reader).lowerBackground.withMargin(borderWidth, 2 * borderWidth, borderWidth, 0)), - background: asCssVariable(editorBackground), - //boxShadow: `${asCssVariable(scrollbarShadow)} 0 6px 6px -6px`, - cursor: 'pointer', - pointerEvents: 'auto', - }, - onmousedown: e => { - e.preventDefault(); // This prevents that the editor loses focus - }, - onmouseup: (e) => this._onDidClick.fire(new StandardMouseEvent(getWindow(e), e)), - obsRef: (elem) => { - this._hoverableElement.set(elem, undefined); - } - }), - n.div({ - style: { - position: 'absolute', - ...rectToProps(reader => layout.read(reader).modifiedLine.withMargin(1, 2)), - fontFamily: this._editor.getOption(EditorOption.fontFamily), - fontSize: this._editor.getOption(EditorOption.fontSize), - fontWeight: this._editor.getOption(EditorOption.fontWeight), - - pointerEvents: 'none', - boxSizing: 'border-box', - borderRadius: '4px', - border: `${borderWidth}px solid ${modifiedBorderColor}`, - - background: asCssVariable(modifiedChangedTextOverlayColor), - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - - outline: `2px solid ${asCssVariable(editorBackground)}`, - } - }, [this._line]), - n.div({ - style: { - position: 'absolute', - ...rectToProps(reader => layout.read(reader).originalLine.withMargin(1)), - boxSizing: 'border-box', - borderRadius: '4px', - border: `${borderWidth}px solid ${originalBorderColor}`, - background: asCssVariable(originalChangedTextOverlayColor), - pointerEvents: 'none', - } - }, []), - - n.svg({ - width: 11, - height: 14, - viewBox: '0 0 11 14', - fill: 'none', - style: { - position: 'absolute', - left: layout.map(l => l.modifiedLine.left - 16), - top: layout.map(l => l.modifiedLine.top + Math.round((l.lineHeight - 14 - 5) / 2)), - } - }, [ - n.svgElem('path', { - d: 'M1 0C1 2.98966 1 5.92087 1 8.49952C1 9.60409 1.89543 10.5 3 10.5H10.5', - stroke: asCssVariable(editorHoverForeground), - }), - n.svgElem('path', { - d: 'M6 7.5L9.99999 10.49998L6 13.5', - stroke: asCssVariable(editorHoverForeground), - }) - ]), - - ]) - ]; - }) - ]).keepUpdated(this._store); + private readonly _root; } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/originalEditorInlineDiffView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/originalEditorInlineDiffView.ts index 0510e23b849..c383f604a9b 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/originalEditorInlineDiffView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/originalEditorInlineDiffView.ts @@ -10,9 +10,9 @@ import { autorunWithStore, derived, IObservable, observableFromEvent } from '../ import { ICodeEditor, MouseTargetType } from '../../../../../../browser/editorBrowser.js'; import { observableCodeEditor } from '../../../../../../browser/observableCodeEditor.js'; import { rangeIsSingleLine } from '../../../../../../browser/widget/diffEditor/components/diffEditorViewZones/diffEditorViewZones.js'; -import { OffsetRange } from '../../../../../../common/core/offsetRange.js'; +import { OffsetRange } from '../../../../../../common/core/ranges/offsetRange.js'; import { Range } from '../../../../../../common/core/range.js'; -import { AbstractText } from '../../../../../../common/core/edits/textEdit.js'; +import { AbstractText } from '../../../../../../common/core/text/abstractText.js'; import { DetailedLineRangeMapping } from '../../../../../../common/diff/rangeMapping.js'; import { EndOfLinePreference, IModelDeltaDecoration, InjectedTextCursorStops, ITextModel } from '../../../../../../common/model.js'; import { ModelDecorationOptions } from '../../../../../../common/model/textModel.js'; @@ -32,17 +32,12 @@ export class OriginalEditorInlineDiffView extends Disposable implements IInlineE return allowsTrueInlineDiffRendering(mapping); } - private readonly _onDidClick = this._register(new Emitter()); - readonly onDidClick = this._onDidClick.event; + private readonly _onDidClick; + readonly onDidClick; - readonly isHovered = observableCodeEditor(this._originalEditor).isTargetHovered( - p => p.target.type === MouseTargetType.CONTENT_TEXT && - p.target.detail.injectedText?.options.attachedData instanceof InlineEditAttachedData && - p.target.detail.injectedText.options.attachedData.owner === this, - this._store - ); + readonly isHovered; - private readonly _tokenizationFinished = modelTokenizationFinished(this._modifiedTextModel); + private readonly _tokenizationFinished; constructor( private readonly _originalEditor: ICodeEditor, @@ -50,6 +45,165 @@ export class OriginalEditorInlineDiffView extends Disposable implements IInlineE private readonly _modifiedTextModel: ITextModel, ) { super(); + this._onDidClick = this._register(new Emitter()); + this.onDidClick = this._onDidClick.event; + this.isHovered = observableCodeEditor(this._originalEditor).isTargetHovered( + p => p.target.type === MouseTargetType.CONTENT_TEXT && + p.target.detail.injectedText?.options.attachedData instanceof InlineEditAttachedData && + p.target.detail.injectedText.options.attachedData.owner === this, + this._store + ); + this._tokenizationFinished = modelTokenizationFinished(this._modifiedTextModel); + this._decorations = derived(this, reader => { + const diff = this._state.read(reader); + if (!diff) { return undefined; } + + const modified = diff.modifiedText; + const showInline = diff.mode === 'insertionInline'; + const hasOneInnerChange = diff.diff.length === 1 && diff.diff[0].innerChanges?.length === 1; + + const showEmptyDecorations = true; + + const originalDecorations: IModelDeltaDecoration[] = []; + const modifiedDecorations: IModelDeltaDecoration[] = []; + + const diffLineAddDecorationBackground = ModelDecorationOptions.register({ + className: 'inlineCompletions-line-insert', + description: 'line-insert', + isWholeLine: true, + marginClassName: 'gutter-insert', + }); + + const diffLineDeleteDecorationBackground = ModelDecorationOptions.register({ + className: 'inlineCompletions-line-delete', + description: 'line-delete', + isWholeLine: true, + marginClassName: 'gutter-delete', + }); + + const diffWholeLineDeleteDecoration = ModelDecorationOptions.register({ + className: 'inlineCompletions-char-delete', + description: 'char-delete', + isWholeLine: false, + }); + + const diffWholeLineAddDecoration = ModelDecorationOptions.register({ + className: 'inlineCompletions-char-insert', + description: 'char-insert', + isWholeLine: true, + }); + + const diffAddDecoration = ModelDecorationOptions.register({ + className: 'inlineCompletions-char-insert', + description: 'char-insert', + shouldFillLineOnLineBreak: true, + }); + + const diffAddDecorationEmpty = ModelDecorationOptions.register({ + className: 'inlineCompletions-char-insert diff-range-empty', + description: 'char-insert diff-range-empty', + }); + + for (const m of diff.diff) { + const showFullLineDecorations = diff.mode !== 'sideBySide' && diff.mode !== 'deletion' && diff.mode !== 'insertionInline'; + if (showFullLineDecorations) { + if (!m.original.isEmpty) { + originalDecorations.push({ + range: m.original.toInclusiveRange()!, + options: diffLineDeleteDecorationBackground, + }); + } + if (!m.modified.isEmpty) { + modifiedDecorations.push({ + range: m.modified.toInclusiveRange()!, + options: diffLineAddDecorationBackground, + }); + } + } + + if (m.modified.isEmpty || m.original.isEmpty) { + if (!m.original.isEmpty) { + originalDecorations.push({ range: m.original.toInclusiveRange()!, options: diffWholeLineDeleteDecoration }); + } + if (!m.modified.isEmpty) { + modifiedDecorations.push({ range: m.modified.toInclusiveRange()!, options: diffWholeLineAddDecoration }); + } + } else { + const useInlineDiff = showInline && allowsTrueInlineDiffRendering(m); + for (const i of m.innerChanges || []) { + // Don't show empty markers outside the line range + if (m.original.contains(i.originalRange.startLineNumber)) { + const replacedText = this._originalEditor.getModel()?.getValueInRange(i.originalRange, EndOfLinePreference.LF); + originalDecorations.push({ + range: i.originalRange, + options: { + description: 'char-delete', + shouldFillLineOnLineBreak: false, + className: classNames( + 'inlineCompletions-char-delete', + i.originalRange.isSingleLine() && diff.mode === 'insertionInline' && 'single-line-inline', + i.originalRange.isEmpty() && 'empty', + ((i.originalRange.isEmpty() && hasOneInnerChange || diff.mode === 'deletion' && replacedText === '\n') && showEmptyDecorations && !useInlineDiff) && 'diff-range-empty' + ), + inlineClassName: useInlineDiff ? classNames('strike-through', 'inlineCompletions') : null, + zIndex: 1 + } + }); + } + if (m.modified.contains(i.modifiedRange.startLineNumber)) { + modifiedDecorations.push({ + range: i.modifiedRange, + options: (i.modifiedRange.isEmpty() && showEmptyDecorations && !useInlineDiff && hasOneInnerChange) + ? diffAddDecorationEmpty + : diffAddDecoration + }); + } + if (useInlineDiff) { + const insertedText = modified.getValueOfRange(i.modifiedRange); + // when the injected text becomes long, the editor will split it into multiple spans + // to be able to get the border around the start and end of the text, we need to split it into multiple segments + const textSegments = insertedText.length > 3 ? + [ + { text: insertedText.slice(0, 1), extraClasses: ['start'], offsetRange: new OffsetRange(i.modifiedRange.startColumn - 1, i.modifiedRange.startColumn) }, + { text: insertedText.slice(1, -1), extraClasses: [], offsetRange: new OffsetRange(i.modifiedRange.startColumn, i.modifiedRange.endColumn - 2) }, + { text: insertedText.slice(-1), extraClasses: ['end'], offsetRange: new OffsetRange(i.modifiedRange.endColumn - 2, i.modifiedRange.endColumn - 1) } + ] : + [ + { text: insertedText, extraClasses: ['start', 'end'], offsetRange: new OffsetRange(i.modifiedRange.startColumn - 1, i.modifiedRange.endColumn) } + ]; + + // Tokenization + this._tokenizationFinished.read(reader); // reconsider when tokenization is finished + const lineTokens = this._modifiedTextModel.tokenization.getLineTokens(i.modifiedRange.startLineNumber); + + for (const { text, extraClasses, offsetRange } of textSegments) { + originalDecorations.push({ + range: Range.fromPositions(i.originalRange.getEndPosition()), + options: { + description: 'inserted-text', + before: { + tokens: lineTokens.getTokensInRange(offsetRange), + content: text, + inlineClassName: classNames( + 'inlineCompletions-char-insert', + i.modifiedRange.isSingleLine() && diff.mode === 'insertionInline' && 'single-line-inline', + ...extraClasses // include extraClasses for additional styling if provided + ), + cursorStops: InjectedTextCursorStops.None, + attachedData: new InlineEditAttachedData(this), + }, + zIndex: 2, + showIfCollapsed: true, + } + }); + } + } + } + } + } + + return { originalDecorations, modifiedDecorations }; + }); this._register(observableCodeEditor(this._originalEditor).setDecorations(this._decorations.map(d => d?.originalDecorations ?? []))); @@ -72,156 +226,7 @@ export class OriginalEditorInlineDiffView extends Disposable implements IInlineE })); } - private readonly _decorations = derived(this, reader => { - const diff = this._state.read(reader); - if (!diff) { return undefined; } - - const modified = diff.modifiedText; - const showInline = diff.mode === 'insertionInline'; - const hasOneInnerChange = diff.diff.length === 1 && diff.diff[0].innerChanges?.length === 1; - - const showEmptyDecorations = true; - - const originalDecorations: IModelDeltaDecoration[] = []; - const modifiedDecorations: IModelDeltaDecoration[] = []; - - const diffLineAddDecorationBackground = ModelDecorationOptions.register({ - className: 'inlineCompletions-line-insert', - description: 'line-insert', - isWholeLine: true, - marginClassName: 'gutter-insert', - }); - - const diffLineDeleteDecorationBackground = ModelDecorationOptions.register({ - className: 'inlineCompletions-line-delete', - description: 'line-delete', - isWholeLine: true, - marginClassName: 'gutter-delete', - }); - - const diffWholeLineDeleteDecoration = ModelDecorationOptions.register({ - className: 'inlineCompletions-char-delete', - description: 'char-delete', - isWholeLine: false, - }); - - const diffWholeLineAddDecoration = ModelDecorationOptions.register({ - className: 'inlineCompletions-char-insert', - description: 'char-insert', - isWholeLine: true, - }); - - const diffAddDecoration = ModelDecorationOptions.register({ - className: 'inlineCompletions-char-insert', - description: 'char-insert', - shouldFillLineOnLineBreak: true, - }); - - const diffAddDecorationEmpty = ModelDecorationOptions.register({ - className: 'inlineCompletions-char-insert diff-range-empty', - description: 'char-insert diff-range-empty', - }); - - for (const m of diff.diff) { - const showFullLineDecorations = diff.mode !== 'sideBySide' && diff.mode !== 'deletion' && diff.mode !== 'insertionInline'; - if (showFullLineDecorations) { - if (!m.original.isEmpty) { - originalDecorations.push({ - range: m.original.toInclusiveRange()!, - options: diffLineDeleteDecorationBackground, - }); - } - if (!m.modified.isEmpty) { - modifiedDecorations.push({ - range: m.modified.toInclusiveRange()!, - options: diffLineAddDecorationBackground, - }); - } - } - - if (m.modified.isEmpty || m.original.isEmpty) { - if (!m.original.isEmpty) { - originalDecorations.push({ range: m.original.toInclusiveRange()!, options: diffWholeLineDeleteDecoration }); - } - if (!m.modified.isEmpty) { - modifiedDecorations.push({ range: m.modified.toInclusiveRange()!, options: diffWholeLineAddDecoration }); - } - } else { - const useInlineDiff = showInline && allowsTrueInlineDiffRendering(m); - for (const i of m.innerChanges || []) { - // Don't show empty markers outside the line range - if (m.original.contains(i.originalRange.startLineNumber)) { - const replacedText = this._originalEditor.getModel()?.getValueInRange(i.originalRange, EndOfLinePreference.LF); - originalDecorations.push({ - range: i.originalRange, - options: { - description: 'char-delete', - shouldFillLineOnLineBreak: false, - className: classNames( - 'inlineCompletions-char-delete', - i.originalRange.isSingleLine() && diff.mode === 'insertionInline' && 'single-line-inline', - i.originalRange.isEmpty() && 'empty', - ((i.originalRange.isEmpty() && hasOneInnerChange || diff.mode === 'deletion' && replacedText === '\n') && showEmptyDecorations && !useInlineDiff) && 'diff-range-empty' - ), - inlineClassName: useInlineDiff ? classNames('strike-through', 'inlineCompletions') : null, - zIndex: 1 - } - }); - } - if (m.modified.contains(i.modifiedRange.startLineNumber)) { - modifiedDecorations.push({ - range: i.modifiedRange, - options: (i.modifiedRange.isEmpty() && showEmptyDecorations && !useInlineDiff && hasOneInnerChange) - ? diffAddDecorationEmpty - : diffAddDecoration - }); - } - if (useInlineDiff) { - const insertedText = modified.getValueOfRange(i.modifiedRange); - // when the injected text becomes long, the editor will split it into multiple spans - // to be able to get the border around the start and end of the text, we need to split it into multiple segments - const textSegments = insertedText.length > 3 ? - [ - { text: insertedText.slice(0, 1), extraClasses: ['start'], offsetRange: new OffsetRange(i.modifiedRange.startColumn - 1, i.modifiedRange.startColumn) }, - { text: insertedText.slice(1, -1), extraClasses: [], offsetRange: new OffsetRange(i.modifiedRange.startColumn, i.modifiedRange.endColumn - 2) }, - { text: insertedText.slice(-1), extraClasses: ['end'], offsetRange: new OffsetRange(i.modifiedRange.endColumn - 2, i.modifiedRange.endColumn - 1) } - ] : - [ - { text: insertedText, extraClasses: ['start', 'end'], offsetRange: new OffsetRange(i.modifiedRange.startColumn - 1, i.modifiedRange.endColumn) } - ]; - - // Tokenization - this._tokenizationFinished.read(reader); // reconsider when tokenization is finished - const lineTokens = this._modifiedTextModel.tokenization.getLineTokens(i.modifiedRange.startLineNumber); - - for (const { text, extraClasses, offsetRange } of textSegments) { - originalDecorations.push({ - range: Range.fromPositions(i.originalRange.getEndPosition()), - options: { - description: 'inserted-text', - before: { - tokens: lineTokens.getTokensInRange(offsetRange), - content: text, - inlineClassName: classNames( - 'inlineCompletions-char-insert', - i.modifiedRange.isSingleLine() && diff.mode === 'insertionInline' && 'single-line-inline', - ...extraClasses // include extraClasses for additional styling if provided - ), - cursorStops: InjectedTextCursorStops.None, - attachedData: new InlineEditAttachedData(this), - }, - zIndex: 2, - showIfCollapsed: true, - } - }); - } - } - } - } - } - - return { originalDecorations, modifiedDecorations }; - }); + private readonly _decorations; } class InlineEditAttachedData { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils/utils.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils/utils.ts index 15a4269393d..eab1daddfc2 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils/utils.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils/utils.ts @@ -19,10 +19,10 @@ import { Point } from '../../../../../../common/core/2d/point.js'; import { Rect } from '../../../../../../common/core/2d/rect.js'; import { EditorOption } from '../../../../../../common/config/editorOptions.js'; import { LineRange } from '../../../../../../common/core/ranges/lineRange.js'; -import { OffsetRange } from '../../../../../../common/core/offsetRange.js'; +import { OffsetRange } from '../../../../../../common/core/ranges/offsetRange.js'; import { Position } from '../../../../../../common/core/position.js'; import { Range } from '../../../../../../common/core/range.js'; -import { SingleTextEdit, TextEdit } from '../../../../../../common/core/edits/textEdit.js'; +import { TextReplacement, TextEdit } from '../../../../../../common/core/edits/textEdit.js'; import { RangeMapping } from '../../../../../../common/diff/rangeMapping.js'; import { ITextModel } from '../../../../../../common/model.js'; import { indentOfLine } from '../../../../../../common/model/textModel.js'; @@ -165,10 +165,10 @@ function offsetRangeToRange(columnOffsetRange: OffsetRange, startPos: Position): export function createReindentEdit(text: string, range: LineRange): TextEdit { const newLines = splitLines(text); - const edits: SingleTextEdit[] = []; + const edits: TextReplacement[] = []; const minIndent = findFirstMin(range.mapToLineArray(l => getIndentationLength(newLines[l - 1])), numberComparator)!; range.forEach(lineNumber => { - edits.push(new SingleTextEdit(offsetRangeToRange(new OffsetRange(0, minIndent), new Position(lineNumber, 1)), '')); + edits.push(new TextReplacement(offsetRangeToRange(new OffsetRange(0, minIndent), new Position(lineNumber, 1)), '')); }); return new TextEdit(edits); } diff --git a/src/vs/editor/contrib/inlineCompletions/test/browser/computeGhostText.test.ts b/src/vs/editor/contrib/inlineCompletions/test/browser/computeGhostText.test.ts index 059f5fdb964..b4fbab79c59 100644 --- a/src/vs/editor/contrib/inlineCompletions/test/browser/computeGhostText.test.ts +++ b/src/vs/editor/contrib/inlineCompletions/test/browser/computeGhostText.test.ts @@ -6,7 +6,7 @@ import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; import { Range } from '../../../../common/core/range.js'; -import { SingleTextEdit } from '../../../../common/core/edits/textEdit.js'; +import { TextReplacement } from '../../../../common/core/edits/textEdit.js'; import { createTextModel } from '../../../../test/common/testTextModel.js'; import { computeGhostText } from '../../browser/model/computeGhostText.js'; @@ -22,7 +22,7 @@ suite('computeGhostText', () => { const options = ['prefix', 'subword'] as const; const result = {} as any; for (const option of options) { - result[option] = computeGhostText(new SingleTextEdit(range, suggestion), tempModel, option)?.render(cleanedText, true); + result[option] = computeGhostText(new TextReplacement(range, suggestion), tempModel, option)?.render(cleanedText, true); } tempModel.dispose(); diff --git a/src/vs/editor/contrib/inlineCompletions/test/browser/getSecondaryEdits.test.ts b/src/vs/editor/contrib/inlineCompletions/test/browser/getSecondaryEdits.test.ts index 5e3351542dd..1512dd4a06f 100644 --- a/src/vs/editor/contrib/inlineCompletions/test/browser/getSecondaryEdits.test.ts +++ b/src/vs/editor/contrib/inlineCompletions/test/browser/getSecondaryEdits.test.ts @@ -5,7 +5,7 @@ import assert from 'assert'; import { Position } from '../../../../common/core/position.js'; import { getSecondaryEdits } from '../../browser/model/inlineCompletionsModel.js'; -import { SingleTextEdit } from '../../../../common/core/edits/textEdit.js'; +import { TextReplacement } from '../../../../common/core/edits/textEdit.js'; import { createTextModel } from '../../../../test/common/testTextModel.js'; import { Range } from '../../../../common/core/range.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; @@ -24,9 +24,9 @@ suite('getSecondaryEdits', () => { new Position(1, 14), new Position(2, 14) ]; - const primaryEdit = new SingleTextEdit(new Range(1, 1, 1, 14), 'function fib() {'); + const primaryEdit = new TextReplacement(new Range(1, 1, 1, 14), 'function fib() {'); const secondaryEdits = getSecondaryEdits(textModel, positions, primaryEdit); - assert.deepStrictEqual(secondaryEdits, [new SingleTextEdit( + assert.deepStrictEqual(secondaryEdits, [new TextReplacement( new Range(2, 14, 2, 14), ') {' )]); @@ -45,13 +45,13 @@ suite('getSecondaryEdits', () => { new Position(2, 1), new Position(4, 1) ]; - const primaryEdit = new SingleTextEdit(new Range(1, 1, 2, 1), [ + const primaryEdit = new TextReplacement(new Range(1, 1, 2, 1), [ 'function fib() {', ' return 0;', '}' ].join('\n')); const secondaryEdits = getSecondaryEdits(textModel, positions, primaryEdit); - assert.deepStrictEqual(secondaryEdits, [new SingleTextEdit( + assert.deepStrictEqual(secondaryEdits, [new TextReplacement( new Range(4, 1, 4, 1), [ ' return 0;', '}' @@ -73,14 +73,14 @@ suite('getSecondaryEdits', () => { new Position(2, 1), new Position(4, 1) ]; - const primaryEdit = new SingleTextEdit(new Range(1, 1, 2, 1), [ + const primaryEdit = new TextReplacement(new Range(1, 1, 2, 1), [ 'class A {', ' public x: number = 0;', ' public y: number = 0;', '}' ].join('\n')); const secondaryEdits = getSecondaryEdits(textModel, positions, primaryEdit); - assert.deepStrictEqual(secondaryEdits, [new SingleTextEdit( + assert.deepStrictEqual(secondaryEdits, [new TextReplacement( new Range(4, 1, 4, 1), [ ' public x: number = 0;', ' public y: number = 0;', diff --git a/src/vs/editor/contrib/inlineCompletions/test/browser/utils.ts b/src/vs/editor/contrib/inlineCompletions/test/browser/utils.ts index dcfffc1b208..9c9acbca0fa 100644 --- a/src/vs/editor/contrib/inlineCompletions/test/browser/utils.ts +++ b/src/vs/editor/contrib/inlineCompletions/test/browser/utils.ts @@ -23,7 +23,7 @@ import { InlineCompletionsController } from '../../browser/controller/inlineComp import { Range } from '../../../../common/core/range.js'; import { TextEdit } from '../../../../common/core/edits/textEdit.js'; import { BugIndicatingError } from '../../../../../base/common/errors.js'; -import { PositionOffsetTransformer } from '../../../../common/core/positionToOffset.js'; +import { PositionOffsetTransformer } from '../../../../common/core/text/positionToOffset.js'; export class MockInlineCompletionsProvider implements InlineCompletionsProvider { private returnValue: InlineCompletion[] = []; diff --git a/src/vs/editor/contrib/peekView/browser/peekView.ts b/src/vs/editor/contrib/peekView/browser/peekView.ts index 946f5ec0ecc..ce8921b550f 100644 --- a/src/vs/editor/contrib/peekView/browser/peekView.ts +++ b/src/vs/editor/contrib/peekView/browser/peekView.ts @@ -255,7 +255,7 @@ export abstract class PeekViewWidget extends ZoneWidget { } const headHeight = Math.ceil(this.editor.getOption(EditorOption.lineHeight) * 1.2); - const bodyHeight = Math.round(heightInPixel - (headHeight + 2 /* the border-top/bottom width*/)); + const bodyHeight = Math.round(heightInPixel - (headHeight + 1 /* the border-top width */)); this._doLayoutHead(headHeight, widthInPixel); this._doLayoutBody(bodyHeight, widthInPixel); diff --git a/src/vs/editor/contrib/placeholderText/browser/placeholderTextContribution.ts b/src/vs/editor/contrib/placeholderText/browser/placeholderTextContribution.ts index b06224c3e9d..2520e98824c 100644 --- a/src/vs/editor/contrib/placeholderText/browser/placeholderTextContribution.ts +++ b/src/vs/editor/contrib/placeholderText/browser/placeholderTextContribution.ts @@ -6,7 +6,7 @@ import { h } from '../../../../base/browser/dom.js'; import { structuralEquals } from '../../../../base/common/equals.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; -import { autorun, constObservable, DebugOwner, derivedObservableWithCache, derivedOpts, derivedWithStore, IObservable, IReader } from '../../../../base/common/observable.js'; +import { autorun, constObservable, DebugOwner, derivedObservableWithCache, derivedOpts, derived, IObservable, IReader } from '../../../../base/common/observable.js'; import { ICodeEditor } from '../../../browser/editorBrowser.js'; import { observableCodeEditor } from '../../../browser/observableCodeEditor.js'; import { EditorOption } from '../../../common/config/editorOptions.js'; @@ -21,53 +21,58 @@ export class PlaceholderTextContribution extends Disposable implements IEditorCo } public static readonly ID = 'editor.contrib.placeholderText'; - private readonly _editorObs = observableCodeEditor(this._editor); + private readonly _editorObs; - private readonly _placeholderText = this._editorObs.getOption(EditorOption.placeholder); + private readonly _placeholderText; - private readonly _state = derivedOpts<{ placeholder: string } | undefined>({ owner: this, equalsFn: structuralEquals }, reader => { - const p = this._placeholderText.read(reader); - if (!p) { return undefined; } - if (!this._editorObs.valueIsEmpty.read(reader)) { return undefined; } - return { placeholder: p }; - }); + private readonly _state; - private readonly _shouldViewBeAlive = isOrWasTrue(this, reader => this._state.read(reader)?.placeholder !== undefined); + private readonly _shouldViewBeAlive; - private readonly _view = derivedWithStore((reader, store) => { - if (!this._shouldViewBeAlive.read(reader)) { return; } - - const element = h('div.editorPlaceholder'); - - store.add(autorun(reader => { - const data = this._state.read(reader); - const shouldBeVisibile = data?.placeholder !== undefined; - element.root.style.display = shouldBeVisibile ? 'block' : 'none'; - element.root.innerText = data?.placeholder ?? ''; - })); - store.add(autorun(reader => { - const info = this._editorObs.layoutInfo.read(reader); - element.root.style.left = `${info.contentLeft}px`; - element.root.style.width = (info.contentWidth - info.verticalScrollbarWidth) + 'px'; - element.root.style.top = `${this._editor.getTopForLineNumber(0)}px`; - })); - store.add(autorun(reader => { - element.root.style.fontFamily = this._editorObs.getOption(EditorOption.fontFamily).read(reader); - element.root.style.fontSize = this._editorObs.getOption(EditorOption.fontSize).read(reader) + 'px'; - element.root.style.lineHeight = this._editorObs.getOption(EditorOption.lineHeight).read(reader) + 'px'; - })); - store.add(this._editorObs.createOverlayWidget({ - allowEditorOverflow: false, - minContentWidthInPx: constObservable(0), - position: constObservable(null), - domNode: element.root, - })); - }); + private readonly _view; constructor( private readonly _editor: ICodeEditor, ) { super(); + this._editorObs = observableCodeEditor(this._editor); + this._placeholderText = this._editorObs.getOption(EditorOption.placeholder); + this._state = derivedOpts<{ placeholder: string } | undefined>({ owner: this, equalsFn: structuralEquals }, reader => { + const p = this._placeholderText.read(reader); + if (!p) { return undefined; } + if (!this._editorObs.valueIsEmpty.read(reader)) { return undefined; } + return { placeholder: p }; + }); + this._shouldViewBeAlive = isOrWasTrue(this, reader => this._state.read(reader)?.placeholder !== undefined); + this._view = derived((reader) => { + if (!this._shouldViewBeAlive.read(reader)) { return; } + + const element = h('div.editorPlaceholder'); + + reader.store.add(autorun(reader => { + const data = this._state.read(reader); + const shouldBeVisibile = data?.placeholder !== undefined; + element.root.style.display = shouldBeVisibile ? 'block' : 'none'; + element.root.innerText = data?.placeholder ?? ''; + })); + reader.store.add(autorun(reader => { + const info = this._editorObs.layoutInfo.read(reader); + element.root.style.left = `${info.contentLeft}px`; + element.root.style.width = (info.contentWidth - info.verticalScrollbarWidth) + 'px'; + element.root.style.top = `${this._editor.getTopForLineNumber(0)}px`; + })); + reader.store.add(autorun(reader => { + element.root.style.fontFamily = this._editorObs.getOption(EditorOption.fontFamily).read(reader); + element.root.style.fontSize = this._editorObs.getOption(EditorOption.fontSize).read(reader) + 'px'; + element.root.style.lineHeight = this._editorObs.getOption(EditorOption.lineHeight).read(reader) + 'px'; + })); + reader.store.add(this._editorObs.createOverlayWidget({ + allowEditorOverflow: false, + minContentWidthInPx: constObservable(0), + position: constObservable(null), + domNode: element.root, + })); + }); this._view.recomputeInitiallyAndOnChange(this._store); } } diff --git a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollProvider.ts b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollProvider.ts index 24f9a4c279d..d5d585d073b 100644 --- a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollProvider.ts +++ b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollProvider.ts @@ -15,6 +15,7 @@ import { Event, Emitter } from '../../../../base/common/event.js'; import { ILanguageConfigurationService } from '../../../common/languages/languageConfigurationRegistry.js'; import { StickyModelProvider, IStickyModelProvider } from './stickyScrollModelProvider.js'; import { StickyElement, StickyModel, StickyRange } from './stickyScrollElement.js'; +import { Position } from '../../../common/core/position.js'; export class StickyLineCandidate { constructor( @@ -175,7 +176,7 @@ export class StickyLineCandidateProvider extends Disposable implements IStickyLi const childEndLine = childRange.endLineNumber; if (range.startLineNumber <= childEndLine + 1 && childStartLine - 1 <= range.endLineNumber && childStartLine !== lastLine) { lastLine = childStartLine; - const lineHeight = this._editor.getLineHeightForLineNumber(childStartLine); + const lineHeight = this._editor.getLineHeightForPosition(new Position(childStartLine, 1)); result.push(new StickyLineCandidate(childStartLine, childEndLine - 1, top, lineHeight)); this.getCandidateStickyLinesIntersectingFromStickyModel(range, child, result, depth + 1, top + lineHeight, childStartLine); } diff --git a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts index ddda2843eb2..1abf94328e2 100644 --- a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts +++ b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts @@ -263,7 +263,7 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { private _getHeightOfLines(lineNumbers: number[], lastLineRelativePosition: number): number { let totalHeight = 0; for (let i = 0; i < lineNumbers.length; i++) { - totalHeight += this._editor.getLineHeightForLineNumber(lineNumbers[i]); + totalHeight += this._editor.getLineHeightForPosition(new Position(lineNumbers[i], 1)); } return totalHeight + lastLineRelativePosition; } @@ -318,7 +318,7 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { actualInlineDecorations = []; } - const lineHeight = this._editor.getLineHeightForLineNumber(line); + const lineHeight = this._editor.getLineHeightForPosition(new Position(line, 1)); const renderLineInput: RenderLineInput = new RenderLineInput(true, true, lineRenderingData.content, lineRenderingData.continuesWithWrappedLine, lineRenderingData.isBasicASCII, lineRenderingData.containsRTL, 0, diff --git a/src/vs/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter.ts b/src/vs/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter.ts index 1a25e9d1f37..b4dcc3a8116 100644 --- a/src/vs/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter.ts +++ b/src/vs/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter.ts @@ -214,9 +214,9 @@ function resolveOptions(trusted: boolean, options: InternalUnicodeHighlightOptio } class DocumentUnicodeHighlighter extends Disposable { - private readonly _model: ITextModel = this._editor.getModel(); + private readonly _model: ITextModel; private readonly _updateSoon: RunOnceScheduler; - private _decorations = this._editor.createDecorationsCollection(); + private _decorations; constructor( private readonly _editor: IActiveCodeEditor, @@ -225,6 +225,8 @@ class DocumentUnicodeHighlighter extends Disposable { @IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService, ) { super(); + this._model = this._editor.getModel(); + this._decorations = this._editor.createDecorationsCollection(); this._updateSoon = this._register(new RunOnceScheduler(() => this._update(), 250)); this._register(this._editor.onDidChangeModelContent(() => { @@ -298,9 +300,9 @@ class DocumentUnicodeHighlighter extends Disposable { class ViewportUnicodeHighlighter extends Disposable { - private readonly _model: ITextModel = this._editor.getModel(); + private readonly _model: ITextModel; private readonly _updateSoon: RunOnceScheduler; - private readonly _decorations = this._editor.createDecorationsCollection(); + private readonly _decorations; constructor( private readonly _editor: IActiveCodeEditor, @@ -308,6 +310,8 @@ class ViewportUnicodeHighlighter extends Disposable { private readonly _updateState: (state: IUnicodeHighlightsResult | null) => void, ) { super(); + this._model = this._editor.getModel(); + this._decorations = this._editor.createDecorationsCollection(); this._updateSoon = this._register(new RunOnceScheduler(() => this._update(), 250)); diff --git a/src/vs/editor/standalone/browser/standaloneServices.ts b/src/vs/editor/standalone/browser/standaloneServices.ts index c3208cb7b76..9d886104883 100644 --- a/src/vs/editor/standalone/browser/standaloneServices.ts +++ b/src/vs/editor/standalone/browser/standaloneServices.ts @@ -308,10 +308,6 @@ class StandaloneDialogService implements IDialogService { export class StandaloneNotificationService implements INotificationService { - readonly onDidAddNotification: Event = Event.None; - - readonly onDidRemoveNotification: Event = Event.None; - readonly onDidChangeFilter: Event = Event.None; public _serviceBrand: undefined; diff --git a/src/vs/editor/test/common/codecs/frontMatterDecoder.test.ts b/src/vs/editor/test/common/codecs/frontMatterDecoder.test.ts index 4f39f6ab43c..2e5efd05fa3 100644 --- a/src/vs/editor/test/common/codecs/frontMatterDecoder.test.ts +++ b/src/vs/editor/test/common/codecs/frontMatterDecoder.test.ts @@ -114,4 +114,12 @@ suite('FrontMatterDecoder', () => { new Space(new Range(3, 27, 3, 28)), ]); }); + + test('• empty', async () => { + const test = disposables.add( + new TestFrontMatterDecoder(), + ); + + await test.run('', []); + }); }); diff --git a/src/vs/editor/test/common/core/edit.test.ts b/src/vs/editor/test/common/core/edit.test.ts index fddd5a797cd..521b13dcbed 100644 --- a/src/vs/editor/test/common/core/edit.test.ts +++ b/src/vs/editor/test/common/core/edit.test.ts @@ -7,7 +7,7 @@ import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; import { Random } from './random.js'; import { StringEdit, StringReplacement } from '../../../common/core/edits/stringEdit.js'; -import { OffsetRange } from '../../../common/core/offsetRange.js'; +import { OffsetRange } from '../../../common/core/ranges/offsetRange.js'; import { ArrayEdit, ArrayReplacement } from '../../../common/core/edits/arrayEdit.js'; suite('Edit', () => { @@ -150,8 +150,8 @@ suite('Edit', () => { const e2 = getRandomEdit(s1, rng.nextIntRange(1, 4), rng); - const ae1 = ArrayEdit.create(e1.replacements.map(r => new ArrayReplacement(r.replaceRange, [...r.newValue]))); - const ae2 = ArrayEdit.create(e2.replacements.map(r => new ArrayReplacement(r.replaceRange, [...r.newValue]))); + const ae1 = ArrayEdit.create(e1.replacements.map(r => new ArrayReplacement(r.replaceRange, [...r.newText]))); + const ae2 = ArrayEdit.create(e2.replacements.map(r => new ArrayReplacement(r.replaceRange, [...r.newText]))); const as0 = [...s0]; const as1 = ae1.apply(as0); const as2 = ae2.apply(as1); diff --git a/src/vs/editor/test/common/core/positionOffsetTransformer.test.ts b/src/vs/editor/test/common/core/positionOffsetTransformer.test.ts index 3cf6744390a..f313b7b012e 100644 --- a/src/vs/editor/test/common/core/positionOffsetTransformer.test.ts +++ b/src/vs/editor/test/common/core/positionOffsetTransformer.test.ts @@ -5,8 +5,8 @@ import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; -import { OffsetRange } from '../../../common/core/offsetRange.js'; -import { PositionOffsetTransformer } from '../../../common/core/positionToOffset.js'; +import { OffsetRange } from '../../../common/core/ranges/offsetRange.js'; +import { PositionOffsetTransformer } from '../../../common/core/text/positionToOffset.js'; suite('PositionOffsetTransformer', () => { ensureNoDisposablesAreLeakedInTestSuite(); diff --git a/src/vs/editor/test/common/core/random.ts b/src/vs/editor/test/common/core/random.ts index 6de65b59859..034eecfbcd2 100644 --- a/src/vs/editor/test/common/core/random.ts +++ b/src/vs/editor/test/common/core/random.ts @@ -5,12 +5,13 @@ import { numberComparator } from '../../../../base/common/arrays.js'; import { BugIndicatingError } from '../../../../base/common/errors.js'; -import { OffsetEdit, SingleOffsetEdit } from '../../../common/core/edits/offsetEdit.js'; -import { OffsetRange } from '../../../common/core/offsetRange.js'; +import { StringEdit, StringReplacement } from '../../../common/core/edits/stringEdit.js'; +import { OffsetRange } from '../../../common/core/ranges/offsetRange.js'; import { Position } from '../../../common/core/position.js'; -import { PositionOffsetTransformer } from '../../../common/core/positionToOffset.js'; +import { PositionOffsetTransformer } from '../../../common/core/text/positionToOffset.js'; import { Range } from '../../../common/core/range.js'; -import { AbstractText, SingleTextEdit, TextEdit } from '../../../common/core/edits/textEdit.js'; +import { TextReplacement, TextEdit } from '../../../common/core/edits/textEdit.js'; +import { AbstractText } from '../../../common/core/text/abstractText.js'; export abstract class Random { public static readonly alphabetSmallLowercase = 'abcdefgh'; @@ -70,7 +71,7 @@ export abstract class Random { } public nextTextEdit(target: AbstractText, singleTextEditCount: number): TextEdit { - const singleTextEdits: SingleTextEdit[] = []; + const singleTextEdits: TextReplacement[] = []; const positions = this.nextConsecutivePositions(target, singleTextEditCount * 2); @@ -78,14 +79,14 @@ export abstract class Random { const start = positions[i * 2]; const end = positions[i * 2 + 1]; const newText = this.nextString(end.column - start.column, this.stringGenerator(Random.basicAlphabetMultiline)); - singleTextEdits.push(new SingleTextEdit(Range.fromPositions(start, end), newText)); + singleTextEdits.push(new TextReplacement(Range.fromPositions(start, end), newText)); } return new TextEdit(singleTextEdits).normalize(); } - public nextOffsetEdit(target: string, singleTextEditCount: number, newTextAlphabet = Random.basicAlphabetMultiline): OffsetEdit { - const singleTextEdits: SingleOffsetEdit[] = []; + public nextStringEdit(target: string, singleTextEditCount: number, newTextAlphabet = Random.basicAlphabetMultiline): StringEdit { + const singleTextEdits: StringReplacement[] = []; const positions = this.nextConsecutiveOffsets(new OffsetRange(0, target.length), singleTextEditCount * 2); @@ -96,15 +97,15 @@ export abstract class Random { const newTextLen = this.nextIntRange(range.isEmpty ? 1 : 0, 10); const newText = this.nextString(newTextLen, this.stringGenerator(newTextAlphabet)); - singleTextEdits.push(new SingleOffsetEdit(range, newText)); + singleTextEdits.push(new StringReplacement(range, newText)); } - return new OffsetEdit(singleTextEdits).normalize(); + return new StringEdit(singleTextEdits).normalize(); } - public nextSingleOffsetEdit(target: string, newTextAlphabet = Random.basicAlphabetMultiline): SingleOffsetEdit { - const edit = this.nextOffsetEdit(target, 1, newTextAlphabet); - return edit.edits[0]; + public nextSingleStringEdit(target: string, newTextAlphabet = Random.basicAlphabetMultiline): StringReplacement { + const edit = this.nextStringEdit(target, 1, newTextAlphabet); + return edit.replacements[0]; } } diff --git a/src/vs/editor/test/common/core/textEdit.test.ts b/src/vs/editor/test/common/core/textEdit.test.ts index 6b36ce1ca1d..3528d57fc1e 100644 --- a/src/vs/editor/test/common/core/textEdit.test.ts +++ b/src/vs/editor/test/common/core/textEdit.test.ts @@ -5,8 +5,8 @@ import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; -import { OffsetRange } from '../../../common/core/offsetRange.js'; -import { StringText } from '../../../common/core/edits/textEdit.js'; +import { OffsetRange } from '../../../common/core/ranges/offsetRange.js'; +import { StringText } from '../../../common/core/text/abstractText.js'; import { Random } from './random.js'; suite('TextEdit', () => { diff --git a/src/vs/editor/test/common/model/bracketPairColorizer/combineTextEditInfos.test.ts b/src/vs/editor/test/common/model/bracketPairColorizer/combineTextEditInfos.test.ts index bda37843275..c36882098c8 100644 --- a/src/vs/editor/test/common/model/bracketPairColorizer/combineTextEditInfos.test.ts +++ b/src/vs/editor/test/common/model/bracketPairColorizer/combineTextEditInfos.test.ts @@ -6,7 +6,7 @@ import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; import { Range } from '../../../../common/core/range.js'; -import { SingleTextEdit } from '../../../../common/core/edits/textEdit.js'; +import { TextReplacement } from '../../../../common/core/edits/textEdit.js'; import { TextEditInfo } from '../../../../common/model/bracketPairsTextModelPart/bracketPairsTree/beforeEditPositionMapper.js'; import { combineTextEditInfos } from '../../../../common/model/bracketPairsTextModelPart/bracketPairsTree/combineTextEditInfos.js'; import { lengthAdd, lengthToObj, lengthToPosition, positionToLength, toLength } from '../../../../common/model/bracketPairsTextModelPart/bracketPairsTree/length.js'; @@ -79,7 +79,7 @@ function getRandomEdit(textModel: TextModel, rangeOffsetStart: number, rng: Rand return new TextEditInfo(positionToLength(textModel.getPositionAt(offsetStart)), positionToLength(textModel.getPositionAt(offsetEnd)), toLength(lineCount, columnCount)); } -function toEdit(editInfo: TextEditInfo): SingleTextEdit { +function toEdit(editInfo: TextEditInfo): TextReplacement { const l = lengthToObj(editInfo.newLength); let text = ''; @@ -90,7 +90,7 @@ function toEdit(editInfo: TextEditInfo): SingleTextEdit { text += 'C'; } - return new SingleTextEdit( + return new TextReplacement( Range.fromPositions( lengthToPosition(editInfo.startOffset), lengthToPosition(editInfo.endOffset) diff --git a/src/vs/editor/test/common/model/textModelTokens.test.ts b/src/vs/editor/test/common/model/textModelTokens.test.ts index 8850c911cfd..d96dc73eceb 100644 --- a/src/vs/editor/test/common/model/textModelTokens.test.ts +++ b/src/vs/editor/test/common/model/textModelTokens.test.ts @@ -5,7 +5,7 @@ import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; -import { OffsetRange } from '../../../common/core/offsetRange.js'; +import { OffsetRange } from '../../../common/core/ranges/offsetRange.js'; import { RangePriorityQueueImpl } from '../../../common/model/textModelTokens.js'; suite('RangePriorityQueueImpl', () => { diff --git a/src/vs/editor/test/common/viewLayout/lineHeights.test.ts b/src/vs/editor/test/common/viewLayout/lineHeights.test.ts index 0047a4014ba..19153646275 100644 --- a/src/vs/editor/test/common/viewLayout/lineHeights.test.ts +++ b/src/vs/editor/test/common/viewLayout/lineHeights.test.ts @@ -179,6 +179,17 @@ suite('Editor ViewLayout - LineHeightsManager', () => { assert.strictEqual(manager.heightForLineNumber(5), 10); }); + test('handles deleting lines at the very beginning', () => { + const manager = new LineHeightsManager(10, []); + manager.insertOrChangeCustomLineHeight('decA', 1, 1, 40); + manager.commit(); + + manager.onLinesDeleted(2, 4); // Delete lines 2-4 after the variable line height + + // Check individual line heights + assert.strictEqual(manager.heightForLineNumber(1), 40); + }); + test('handles inserting lines before custom line heights', () => { const manager = new LineHeightsManager(10, []); manager.insertOrChangeCustomLineHeight('dec1', 5, 7, 20); diff --git a/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts b/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts index 778e7418b68..1d2a6ab5002 100644 --- a/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts +++ b/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts @@ -13,7 +13,7 @@ import { LineDecoration } from '../../../common/viewLayout/lineDecorations.js'; import { CharacterMapping, DomPosition, RenderLineInput, RenderLineOutput2, renderViewLine2 as renderViewLine } from '../../../common/viewLayout/viewLineRenderer.js'; import { InlineDecorationType } from '../../../common/viewModel.js'; import { TestLineToken, TestLineTokens } from '../core/testLineToken.js'; -import { OffsetRange } from '../../../common/core/offsetRange.js'; +import { OffsetRange } from '../../../common/core/ranges/offsetRange.js'; function createViewLineTokens(viewLineTokens: TestLineToken[]): IViewLineTokens { return new TestLineTokens(viewLineTokens); diff --git a/src/vs/editor/test/node/diffing/defaultLinesDiffComputer.test.ts b/src/vs/editor/test/node/diffing/defaultLinesDiffComputer.test.ts index 4ecde34435e..9537819b77e 100644 --- a/src/vs/editor/test/node/diffing/defaultLinesDiffComputer.test.ts +++ b/src/vs/editor/test/node/diffing/defaultLinesDiffComputer.test.ts @@ -6,12 +6,12 @@ import assert from 'assert'; import { Range } from '../../../common/core/range.js'; import { getLineRangeMapping, RangeMapping } from '../../../common/diff/rangeMapping.js'; -import { OffsetRange } from '../../../common/core/offsetRange.js'; +import { OffsetRange } from '../../../common/core/ranges/offsetRange.js'; import { LinesSliceCharSequence } from '../../../common/diff/defaultLinesDiffComputer/linesSliceCharSequence.js'; import { MyersDiffAlgorithm } from '../../../common/diff/defaultLinesDiffComputer/algorithms/myersDiffAlgorithm.js'; import { DynamicProgrammingDiffing } from '../../../common/diff/defaultLinesDiffComputer/algorithms/dynamicProgrammingDiffing.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; -import { ArrayText } from '../../../common/core/edits/textEdit.js'; +import { ArrayText } from '../../../common/core/text/abstractText.js'; suite('myers', () => { ensureNoDisposablesAreLeakedInTestSuite(); diff --git a/src/vs/editor/test/node/diffing/fixtures.test.ts b/src/vs/editor/test/node/diffing/fixtures.test.ts index 34be0e4398c..ca4fe8ceacb 100644 --- a/src/vs/editor/test/node/diffing/fixtures.test.ts +++ b/src/vs/editor/test/node/diffing/fixtures.test.ts @@ -13,7 +13,8 @@ import { LegacyLinesDiffComputer } from '../../../common/diff/legacyLinesDiffCom import { DefaultLinesDiffComputer } from '../../../common/diff/defaultLinesDiffComputer/defaultLinesDiffComputer.js'; import { Range } from '../../../common/core/range.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; -import { AbstractText, ArrayText, SingleTextEdit, TextEdit } from '../../../common/core/edits/textEdit.js'; +import { TextReplacement, TextEdit } from '../../../common/core/edits/textEdit.js'; +import { AbstractText, ArrayText } from '../../../common/core/text/abstractText.js'; import { LinesDiff } from '../../../common/diff/linesDiffComputer.js'; suite('diffing fixtures', () => { @@ -181,7 +182,7 @@ function assertDiffCorrectness(diff: LinesDiff, original: string[], modified: st function rangeMappingsToTextEdit(rangeMappings: readonly RangeMapping[], modified: AbstractText): TextEdit { return new TextEdit(rangeMappings.map(m => { - return new SingleTextEdit( + return new TextReplacement( m.originalRange, modified.getValueOfRange(m.modifiedRange) ); diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 0116beab8b8..da48a13c9e8 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -1745,7 +1745,7 @@ declare namespace monaco.editor { */ glyphMargin?: IModelDecorationGlyphMarginOptions | null; /** - * If set, the decoration will override the line height of the lines it spans. + * If set, the decoration will override the line height of the lines it spans. Maximum value is 300px. */ lineHeight?: number | null; /** @@ -3115,6 +3115,10 @@ declare namespace monaco.editor { * This editor is used inside a diff editor. */ inDiffEditor?: boolean; + /** + * This editor is allowed to use variable line heights. + */ + allowVariableLineHeights?: boolean; /** * The aria label for the editor's textarea (when it is focused). */ @@ -4907,159 +4911,160 @@ declare namespace monaco.editor { acceptSuggestionOnEnter = 1, accessibilitySupport = 2, accessibilityPageSize = 3, - ariaLabel = 4, - ariaRequired = 5, - autoClosingBrackets = 6, - autoClosingComments = 7, - screenReaderAnnounceInlineSuggestion = 8, - autoClosingDelete = 9, - autoClosingOvertype = 10, - autoClosingQuotes = 11, - autoIndent = 12, - automaticLayout = 13, - autoSurround = 14, - bracketPairColorization = 15, - guides = 16, - codeLens = 17, - codeLensFontFamily = 18, - codeLensFontSize = 19, - colorDecorators = 20, - colorDecoratorsLimit = 21, - columnSelection = 22, - comments = 23, - contextmenu = 24, - copyWithSyntaxHighlighting = 25, - cursorBlinking = 26, - cursorSmoothCaretAnimation = 27, - cursorStyle = 28, - cursorSurroundingLines = 29, - cursorSurroundingLinesStyle = 30, - cursorWidth = 31, - disableLayerHinting = 32, - disableMonospaceOptimizations = 33, - domReadOnly = 34, - dragAndDrop = 35, - dropIntoEditor = 36, - experimentalEditContextEnabled = 37, - emptySelectionClipboard = 38, - experimentalGpuAcceleration = 39, - experimentalWhitespaceRendering = 40, - extraEditorClassName = 41, - fastScrollSensitivity = 42, - find = 43, - fixedOverflowWidgets = 44, - folding = 45, - foldingStrategy = 46, - foldingHighlight = 47, - foldingImportsByDefault = 48, - foldingMaximumRegions = 49, - unfoldOnClickAfterEndOfLine = 50, - fontFamily = 51, - fontInfo = 52, - fontLigatures = 53, - fontSize = 54, - fontWeight = 55, - fontVariations = 56, - formatOnPaste = 57, - formatOnType = 58, - glyphMargin = 59, - gotoLocation = 60, - hideCursorInOverviewRuler = 61, - hover = 62, - inDiffEditor = 63, - inlineSuggest = 64, - letterSpacing = 65, - lightbulb = 66, - lineDecorationsWidth = 67, - lineHeight = 68, - lineNumbers = 69, - lineNumbersMinChars = 70, - linkedEditing = 71, - links = 72, - matchBrackets = 73, - minimap = 74, - mouseStyle = 75, - mouseWheelScrollSensitivity = 76, - mouseWheelZoom = 77, - multiCursorMergeOverlapping = 78, - multiCursorModifier = 79, - multiCursorPaste = 80, - multiCursorLimit = 81, - occurrencesHighlight = 82, - occurrencesHighlightDelay = 83, - overtypeCursorStyle = 84, - overtypeOnPaste = 85, - overviewRulerBorder = 86, - overviewRulerLanes = 87, - padding = 88, - pasteAs = 89, - parameterHints = 90, - peekWidgetDefaultFocus = 91, - placeholder = 92, - definitionLinkOpensInPeek = 93, - quickSuggestions = 94, - quickSuggestionsDelay = 95, - readOnly = 96, - readOnlyMessage = 97, - renameOnType = 98, - renderControlCharacters = 99, - renderFinalNewline = 100, - renderLineHighlight = 101, - renderLineHighlightOnlyWhenFocus = 102, - renderValidationDecorations = 103, - renderWhitespace = 104, - revealHorizontalRightPadding = 105, - roundedSelection = 106, - rulers = 107, - scrollbar = 108, - scrollBeyondLastColumn = 109, - scrollBeyondLastLine = 110, - scrollPredominantAxis = 111, - selectionClipboard = 112, - selectionHighlight = 113, - selectOnLineNumbers = 114, - showFoldingControls = 115, - showUnused = 116, - snippetSuggestions = 117, - smartSelect = 118, - smoothScrolling = 119, - stickyScroll = 120, - stickyTabStops = 121, - stopRenderingLineAfter = 122, - suggest = 123, - suggestFontSize = 124, - suggestLineHeight = 125, - suggestOnTriggerCharacters = 126, - suggestSelection = 127, - tabCompletion = 128, - tabIndex = 129, - unicodeHighlighting = 130, - unusualLineTerminators = 131, - useShadowDOM = 132, - useTabStops = 133, - wordBreak = 134, - wordSegmenterLocales = 135, - wordSeparators = 136, - wordWrap = 137, - wordWrapBreakAfterCharacters = 138, - wordWrapBreakBeforeCharacters = 139, - wordWrapColumn = 140, - wordWrapOverride1 = 141, - wordWrapOverride2 = 142, - wrappingIndent = 143, - wrappingStrategy = 144, - showDeprecated = 145, - inlayHints = 146, - effectiveCursorStyle = 147, - editorClassName = 148, - pixelRatio = 149, - tabFocusMode = 150, - layoutInfo = 151, - wrappingInfo = 152, - defaultColorDecorators = 153, - colorDecoratorsActivatedOn = 154, - inlineCompletionsAccessibilityVerbose = 155, - effectiveExperimentalEditContextEnabled = 156 + allowVariableLineHeights = 4, + ariaLabel = 5, + ariaRequired = 6, + autoClosingBrackets = 7, + autoClosingComments = 8, + screenReaderAnnounceInlineSuggestion = 9, + autoClosingDelete = 10, + autoClosingOvertype = 11, + autoClosingQuotes = 12, + autoIndent = 13, + automaticLayout = 14, + autoSurround = 15, + bracketPairColorization = 16, + guides = 17, + codeLens = 18, + codeLensFontFamily = 19, + codeLensFontSize = 20, + colorDecorators = 21, + colorDecoratorsLimit = 22, + columnSelection = 23, + comments = 24, + contextmenu = 25, + copyWithSyntaxHighlighting = 26, + cursorBlinking = 27, + cursorSmoothCaretAnimation = 28, + cursorStyle = 29, + cursorSurroundingLines = 30, + cursorSurroundingLinesStyle = 31, + cursorWidth = 32, + disableLayerHinting = 33, + disableMonospaceOptimizations = 34, + domReadOnly = 35, + dragAndDrop = 36, + dropIntoEditor = 37, + experimentalEditContextEnabled = 38, + emptySelectionClipboard = 39, + experimentalGpuAcceleration = 40, + experimentalWhitespaceRendering = 41, + extraEditorClassName = 42, + fastScrollSensitivity = 43, + find = 44, + fixedOverflowWidgets = 45, + folding = 46, + foldingStrategy = 47, + foldingHighlight = 48, + foldingImportsByDefault = 49, + foldingMaximumRegions = 50, + unfoldOnClickAfterEndOfLine = 51, + fontFamily = 52, + fontInfo = 53, + fontLigatures = 54, + fontSize = 55, + fontWeight = 56, + fontVariations = 57, + formatOnPaste = 58, + formatOnType = 59, + glyphMargin = 60, + gotoLocation = 61, + hideCursorInOverviewRuler = 62, + hover = 63, + inDiffEditor = 64, + inlineSuggest = 65, + letterSpacing = 66, + lightbulb = 67, + lineDecorationsWidth = 68, + lineHeight = 69, + lineNumbers = 70, + lineNumbersMinChars = 71, + linkedEditing = 72, + links = 73, + matchBrackets = 74, + minimap = 75, + mouseStyle = 76, + mouseWheelScrollSensitivity = 77, + mouseWheelZoom = 78, + multiCursorMergeOverlapping = 79, + multiCursorModifier = 80, + multiCursorPaste = 81, + multiCursorLimit = 82, + occurrencesHighlight = 83, + occurrencesHighlightDelay = 84, + overtypeCursorStyle = 85, + overtypeOnPaste = 86, + overviewRulerBorder = 87, + overviewRulerLanes = 88, + padding = 89, + pasteAs = 90, + parameterHints = 91, + peekWidgetDefaultFocus = 92, + placeholder = 93, + definitionLinkOpensInPeek = 94, + quickSuggestions = 95, + quickSuggestionsDelay = 96, + readOnly = 97, + readOnlyMessage = 98, + renameOnType = 99, + renderControlCharacters = 100, + renderFinalNewline = 101, + renderLineHighlight = 102, + renderLineHighlightOnlyWhenFocus = 103, + renderValidationDecorations = 104, + renderWhitespace = 105, + revealHorizontalRightPadding = 106, + roundedSelection = 107, + rulers = 108, + scrollbar = 109, + scrollBeyondLastColumn = 110, + scrollBeyondLastLine = 111, + scrollPredominantAxis = 112, + selectionClipboard = 113, + selectionHighlight = 114, + selectOnLineNumbers = 115, + showFoldingControls = 116, + showUnused = 117, + snippetSuggestions = 118, + smartSelect = 119, + smoothScrolling = 120, + stickyScroll = 121, + stickyTabStops = 122, + stopRenderingLineAfter = 123, + suggest = 124, + suggestFontSize = 125, + suggestLineHeight = 126, + suggestOnTriggerCharacters = 127, + suggestSelection = 128, + tabCompletion = 129, + tabIndex = 130, + unicodeHighlighting = 131, + unusualLineTerminators = 132, + useShadowDOM = 133, + useTabStops = 134, + wordBreak = 135, + wordSegmenterLocales = 136, + wordSeparators = 137, + wordWrap = 138, + wordWrapBreakAfterCharacters = 139, + wordWrapBreakBeforeCharacters = 140, + wordWrapColumn = 141, + wordWrapOverride1 = 142, + wordWrapOverride2 = 143, + wrappingIndent = 144, + wrappingStrategy = 145, + showDeprecated = 146, + inlayHints = 147, + effectiveCursorStyle = 148, + editorClassName = 149, + pixelRatio = 150, + tabFocusMode = 151, + layoutInfo = 152, + wrappingInfo = 153, + defaultColorDecorators = 154, + colorDecoratorsActivatedOn = 155, + inlineCompletionsAccessibilityVerbose = 156, + effectiveExperimentalEditContextEnabled = 157 } export const EditorOptions: { @@ -5067,6 +5072,7 @@ declare namespace monaco.editor { acceptSuggestionOnEnter: IEditorOption; accessibilitySupport: IEditorOption; accessibilityPageSize: IEditorOption; + allowVariableLineHeights: IEditorOption; ariaLabel: IEditorOption; ariaRequired: IEditorOption; screenReaderAnnounceInlineSuggestion: IEditorOption; @@ -6112,7 +6118,7 @@ declare namespace monaco.editor { /** * Get the line height for the line number. */ - getLineHeightForLineNumber(lineNumber: number): number; + getLineHeightForPosition(position: IPosition): number; /** * Write the screen reader content to be the current selection */ diff --git a/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts b/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts index 128c3fe2064..2aa904b7327 100644 --- a/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts +++ b/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts @@ -65,12 +65,9 @@ export interface IAccessbilitySignalOptions { export class AccessibilitySignalService extends Disposable implements IAccessibilitySignalService { readonly _serviceBrand: undefined; - private readonly sounds: Map = new Map(); - private readonly screenReaderAttached = observableFromEvent(this, - this.accessibilityService.onDidChangeScreenReaderOptimized, - () => /** @description accessibilityService.onDidChangeScreenReaderOptimized */ this.accessibilityService.isScreenReaderOptimized() - ); - private readonly sentTelemetry = new Set(); + private readonly sounds: Map; + private readonly screenReaderAttached; + private readonly sentTelemetry; constructor( @IConfigurationService private readonly configurationService: IConfigurationService, @@ -78,6 +75,38 @@ export class AccessibilitySignalService extends Disposable implements IAccessibi @ITelemetryService private readonly telemetryService: ITelemetryService, ) { super(); + this.sounds = new Map(); + this.screenReaderAttached = observableFromEvent(this, + this.accessibilityService.onDidChangeScreenReaderOptimized, + () => /** @description accessibilityService.onDidChangeScreenReaderOptimized */ this.accessibilityService.isScreenReaderOptimized() + ); + this.sentTelemetry = new Set(); + this.playingSounds = new Set(); + this._signalConfigValue = new CachedFunction((signal: AccessibilitySignal) => observableConfigValue<{ + sound: EnabledState; + announcement: EnabledState; + }>(signal.settingsKey, { sound: 'off', announcement: 'off' }, this.configurationService)); + this._signalEnabledState = new CachedFunction( + { getCacheKey: getStructuralKey }, + (arg: { signal: AccessibilitySignal; userGesture: boolean; modality?: AccessibilityModality | undefined }) => { + return derived(reader => { + /** @description sound enabled */ + const setting = this._signalConfigValue.get(arg.signal).read(reader); + + if (arg.modality === 'sound' || arg.modality === undefined) { + if (checkEnabledState(setting.sound, () => this.screenReaderAttached.read(reader), arg.userGesture)) { + return true; + } + } + if (arg.modality === 'announcement' || arg.modality === undefined) { + if (checkEnabledState(setting.announcement, () => this.screenReaderAttached.read(reader), arg.userGesture)) { + return true; + } + } + return false; + }).recomputeInitiallyAndOnChange(this._store); + } + ); } public getEnabledState(signal: AccessibilitySignal, userGesture: boolean, modality?: AccessibilityModality | undefined): IValueWithChangeEvent { @@ -152,7 +181,7 @@ export class AccessibilitySignalService extends Disposable implements IAccessibi return Math.max(Math.min(volume, 100), 0); } - private readonly playingSounds = new Set(); + private readonly playingSounds; public async playSound(sound: Sound, allowManyInParallel = false): Promise { if (!allowManyInParallel && this.playingSounds.has(sound)) { @@ -198,32 +227,9 @@ export class AccessibilitySignalService extends Disposable implements IAccessibi return toDisposable(() => playing = false); } - private readonly _signalConfigValue = new CachedFunction((signal: AccessibilitySignal) => observableConfigValue<{ - sound: EnabledState; - announcement: EnabledState; - }>(signal.settingsKey, { sound: 'off', announcement: 'off' }, this.configurationService)); + private readonly _signalConfigValue; - private readonly _signalEnabledState = new CachedFunction( - { getCacheKey: getStructuralKey }, - (arg: { signal: AccessibilitySignal; userGesture: boolean; modality?: AccessibilityModality | undefined }) => { - return derived(reader => { - /** @description sound enabled */ - const setting = this._signalConfigValue.get(arg.signal).read(reader); - - if (arg.modality === 'sound' || arg.modality === undefined) { - if (checkEnabledState(setting.sound, () => this.screenReaderAttached.read(reader), arg.userGesture)) { - return true; - } - } - if (arg.modality === 'announcement' || arg.modality === undefined) { - if (checkEnabledState(setting.announcement, () => this.screenReaderAttached.read(reader), arg.userGesture)) { - return true; - } - } - return false; - }).recomputeInitiallyAndOnChange(this._store); - } - ); + private readonly _signalEnabledState; public isAnnouncementEnabled(signal: AccessibilitySignal, userGesture?: boolean): boolean { if (!signal.announcementMessage) { diff --git a/src/vs/platform/actionWidget/browser/actionList.ts b/src/vs/platform/actionWidget/browser/actionList.ts index 4205ba69d1e..40cd4e39b74 100644 --- a/src/vs/platform/actionWidget/browser/actionList.ts +++ b/src/vs/platform/actionWidget/browser/actionList.ts @@ -131,7 +131,12 @@ class ActionItemRenderer implements IListRenderer, IAction data.text.textContent = stripNewlines(element.label); - if (element.description) { + // if there is a keybinding, prioritize over description for now + if (element.keybinding) { + data.description!.textContent = element.keybinding.getLabel(); + data.description!.style.display = 'inline'; + data.description!.style.letterSpacing = '0.5px'; + } else if (element.description) { data.description!.textContent = stripNewlines(element.description); data.description!.style.display = 'inline'; } else { @@ -139,9 +144,6 @@ class ActionItemRenderer implements IListRenderer, IAction data.description!.style.display = 'none'; } - data.keybinding.set(element.keybinding); - dom.setVisibility(!!element.keybinding, data.keybinding.element); - const actionTitle = this._keybindingService.lookupKeybinding(acceptSelectedActionCommand)?.getLabel(); const previewTitle = this._keybindingService.lookupKeybinding(previewSelectedActionCommand)?.getLabel(); data.container.classList.toggle('option-disabled', element.disabled); diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index c143e4ee1e2..0a312481177 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -126,6 +126,7 @@ export class MenuId { static readonly SCMSourceControlTitle = new MenuId('SCMSourceControlTitle'); static readonly SCMHistoryTitle = new MenuId('SCMHistoryTitle'); static readonly SCMHistoryItemContext = new MenuId('SCMHistoryItemContext'); + static readonly SCMHistoryItemChangeContext = new MenuId('SCMHistoryItemChangeContext'); static readonly SCMHistoryItemHover = new MenuId('SCMHistoryItemHover'); static readonly SCMHistoryItemRefContext = new MenuId('SCMHistoryItemRefContext'); static readonly SCMQuickDiffDecorations = new MenuId('SCMQuickDiffDecorations'); diff --git a/src/vs/platform/browserElements/electron-main/nativeBrowserElementsMainService.ts b/src/vs/platform/browserElements/electron-main/nativeBrowserElementsMainService.ts index 757db569244..561171e9bda 100644 --- a/src/vs/platform/browserElements/electron-main/nativeBrowserElementsMainService.ts +++ b/src/vs/platform/browserElements/electron-main/nativeBrowserElementsMainService.ts @@ -62,8 +62,12 @@ export class NativeBrowserElementsMainService extends Disposable implements INat try { // find parent id and extract id const matchingTarget = targetInfos.find((targetInfo: { url: string }) => { - const url = new URL(targetInfo.url); - return url.searchParams.get('parentId') === window?.id.toString() && url.searchParams.get('extensionId') === 'vscode.simple-browser'; + try { + const url = new URL(targetInfo.url); + return url.searchParams.get('parentId') === window?.id.toString() && url.searchParams.get('extensionId') === 'vscode.simple-browser'; + } catch (err) { + return false; + } }); if (matchingTarget) { @@ -74,8 +78,12 @@ export class NativeBrowserElementsMainService extends Disposable implements INat // use id to grab simple browser target if (resultId) { target = targetInfos.find((targetInfo: { url: string }) => { - const url = new URL(targetInfo.url); - return url.searchParams.get('id') === resultId && url.searchParams.get('vscodeBrowserReqId')!; + try { + const url = new URL(targetInfo.url); + return (url.searchParams.get('id') === resultId && url.searchParams.has('vscodeBrowserReqId')); + } catch (e) { + return false; + } }); } diff --git a/src/vs/platform/environment/common/argv.ts b/src/vs/platform/environment/common/argv.ts index e4522f6492e..ead93c8ec0b 100644 --- a/src/vs/platform/environment/common/argv.ts +++ b/src/vs/platform/environment/common/argv.ts @@ -124,6 +124,7 @@ export interface NativeParsedArgs { 'enable-coi'?: boolean; 'unresponsive-sample-interval'?: string; 'unresponsive-sample-period'?: string; + 'enable-rdp-display-tracking'?: boolean; // chromium command line args: https://electronjs.org/docs/all#supported-chrome-command-line-switches 'no-proxy-server'?: boolean; diff --git a/src/vs/platform/environment/electron-main/environmentMainService.ts b/src/vs/platform/environment/electron-main/environmentMainService.ts index 6fe1e58d11a..08c5abbc8f8 100644 --- a/src/vs/platform/environment/electron-main/environmentMainService.ts +++ b/src/vs/platform/environment/electron-main/environmentMainService.ts @@ -33,6 +33,9 @@ export interface IEnvironmentMainService extends INativeEnvironmentService { // --- config readonly disableUpdates: boolean; + // TODO@deepak1556 TODO@bpasero temporary until a real fix lands upstream + readonly enableRDPDisplayTracking: boolean; + unsetSnapExportedVariables(): void; restoreSnapExportedVariables(): void; } @@ -56,6 +59,9 @@ export class EnvironmentMainService extends NativeEnvironmentService implements @memoize get crossOriginIsolated(): boolean { return !!this.args['enable-coi']; } + @memoize + get enableRDPDisplayTracking(): boolean { return !!this.args['enable-rdp-display-tracking']; } + @memoize get codeCachePath(): string | undefined { return process.env['VSCODE_CODE_CACHE_PATH'] || undefined; } diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index 7ea314341a3..d41a2e9fbd1 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -185,6 +185,7 @@ export const OPTIONS: OptionDescriptions> = { 'enable-coi': { type: 'boolean' }, 'unresponsive-sample-interval': { type: 'string' }, 'unresponsive-sample-period': { type: 'string' }, + 'enable-rdp-display-tracking': { type: 'boolean' }, // chromium flags 'no-proxy-server': { type: 'boolean' }, @@ -237,7 +238,8 @@ const ignoringReporter = { }; export function parseArgs(args: string[], options: OptionDescriptions, errorReporter: ErrorReporter = ignoringReporter): T { - const firstArg = args.find(a => a.length > 0 && a[0] !== '-'); + // Find the first non-option arg, which also isn't the value for a previous `--flag` + const firstPossibleCommand = args.find((a, i) => a.length > 0 && a[0] !== '-' && options.hasOwnProperty(a) && options[a as T].type === 'subcommand'); const alias: { [key: string]: string } = {}; const stringOptions: string[] = ['_']; @@ -247,7 +249,7 @@ export function parseArgs(args: string[], options: OptionDescriptions, err for (const optionId in options) { const o = options[optionId]; if (o.type === 'subcommand') { - if (optionId === firstArg) { + if (optionId === firstPossibleCommand) { command = o; } } else { @@ -271,17 +273,17 @@ export function parseArgs(args: string[], options: OptionDescriptions, err } } } - if (command && firstArg) { + if (command && firstPossibleCommand) { const options = globalOptions; for (const optionId in command.options) { options[optionId] = command.options[optionId]; } - const newArgs = args.filter(a => a !== firstArg); - const reporter = errorReporter.getSubcommandReporter ? errorReporter.getSubcommandReporter(firstArg) : undefined; + const newArgs = args.filter(a => a !== firstPossibleCommand); + const reporter = errorReporter.getSubcommandReporter ? errorReporter.getSubcommandReporter(firstPossibleCommand) : undefined; const subcommandOptions = parseArgs(newArgs, options, reporter); // eslint-disable-next-line local/code-no-dangerous-type-assertions return { - [firstArg]: subcommandOptions, + [firstPossibleCommand]: subcommandOptions, _: [] }; } diff --git a/src/vs/platform/environment/test/node/argv.test.ts b/src/vs/platform/environment/test/node/argv.test.ts index f5fd600d20f..6147f6ecc56 100644 --- a/src/vs/platform/environment/test/node/argv.test.ts +++ b/src/vs/platform/environment/test/node/argv.test.ts @@ -135,6 +135,27 @@ suite('parseArgs', () => { ['testcmd-onUnknownOption testX'] ); + assertParse( + options1, + ['--testArg=foo', 'testcmd', '--testX'], + { testcmd: { testArg: 'foo', '_': [] }, '_': [] }, + ['testcmd-onUnknownOption testX'] + ); + + assertParse( + options1, + ['--testArg=foo', 'testcmd'], + { testcmd: { testArg: 'foo', '_': [] }, '_': [] }, + [] + ); + + assertParse( + options1, + ['--testArg', 'foo', 'testcmd'], + { testcmd: { testArg: 'foo', '_': [] }, '_': [] }, + [] + ); + interface TestArgs2 { testcmd?: { testArg?: string; diff --git a/src/vs/platform/extensions/common/extensions.ts b/src/vs/platform/extensions/common/extensions.ts index f0a4ef36557..9974a54d333 100644 --- a/src/vs/platform/extensions/common/extensions.ts +++ b/src/vs/platform/extensions/common/extensions.ts @@ -104,6 +104,7 @@ export interface ICodeActionContribution { export interface IAuthenticationContribution { readonly id: string; readonly label: string; + readonly issuerGlobs?: string[]; } export interface IWalkthroughStep { diff --git a/src/vs/platform/extensions/common/extensionsApiProposals.ts b/src/vs/platform/extensions/common/extensionsApiProposals.ts index 77d4f0f0b20..180bc1b1e88 100644 --- a/src/vs/platform/extensions/common/extensionsApiProposals.ts +++ b/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -19,6 +19,9 @@ const _allApiProposals = { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.aiTextSearchProvider.d.ts', version: 2 }, + authIssuers: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.authIssuers.d.ts', + }, authLearnMore: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.authLearnMore.d.ts', }, @@ -254,9 +257,6 @@ const _allApiProposals = { notebookCellExecution: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookCellExecution.d.ts', }, - notebookCellExecutionState: { - proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookCellExecutionState.d.ts', - }, notebookControllerAffinityHidden: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookControllerAffinityHidden.d.ts', }, diff --git a/src/vs/platform/files/browser/htmlFileSystemProvider.ts b/src/vs/platform/files/browser/htmlFileSystemProvider.ts index 345d5041933..594934e703c 100644 --- a/src/vs/platform/files/browser/htmlFileSystemProvider.ts +++ b/src/vs/platform/files/browser/htmlFileSystemProvider.ts @@ -221,7 +221,7 @@ export class HTMLFileSystemProvider extends Disposable implements IFileSystemPro // Write to target overwriting any existing contents const writable = await handle.createWritable(); - await writable.write(content); + await writable.write(content as Uint8Array); await writable.close(); } catch (error) { throw this.toFileSystemProviderError(error); diff --git a/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts b/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts index 04af18c812b..075a5f5494c 100644 --- a/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts +++ b/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts @@ -159,8 +159,6 @@ suite('AbstractKeybindingService', () => { const notificationService: INotificationService = { _serviceBrand: undefined, - onDidAddNotification: undefined!, - onDidRemoveNotification: undefined!, onDidChangeFilter: undefined!, notify: (notification: INotification) => { showMessageCalls.push({ sev: notification.severity, message: notification.message }); diff --git a/src/vs/platform/mcp/common/mcpGalleryService.ts b/src/vs/platform/mcp/common/mcpGalleryService.ts new file mode 100644 index 00000000000..d134fc0c0f5 --- /dev/null +++ b/src/vs/platform/mcp/common/mcpGalleryService.ts @@ -0,0 +1,215 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from '../../../base/common/cancellation.js'; +import { Disposable } from '../../../base/common/lifecycle.js'; +import { Schemas } from '../../../base/common/network.js'; +import { dirname, joinPath } from '../../../base/common/resources.js'; +import { uppercaseFirstLetter } from '../../../base/common/strings.js'; +import { URI } from '../../../base/common/uri.js'; +import { localize } from '../../../nls.js'; +import { IConfigurationService } from '../../configuration/common/configuration.js'; +import { IFileService } from '../../files/common/files.js'; +import { ILogService } from '../../log/common/log.js'; +import { IProductService } from '../../product/common/productService.js'; +import { asJson, asText, IRequestService } from '../../request/common/request.js'; +import { IGalleryMcpServer, IMcpGalleryService, IMcpServerManifest, IQueryOptions, mcpGalleryServiceUrlConfig, PackageType } from './mcpManagement.js'; + +interface IRawGalleryMcpServer { + readonly id: string; + readonly name: string; + readonly description: string; + readonly displayName?: string; + readonly repository: { + readonly url: string; + readonly source: string; + }; + readonly version_detail: { + readonly version: string; + readonly release_date: string; + readonly is_latest: boolean; + }; + readonly readmeUrl: string; + readonly publisher?: { + readonly displayName: string; + readonly url: string; + readonly is_verified: boolean; + }; + readonly package_types?: readonly PackageType[]; +} + +type RawGalleryMcpServerManifest = IRawGalleryMcpServer & IMcpServerManifest; + +export class McpGalleryService extends Disposable implements IMcpGalleryService { + + _serviceBrand: undefined; + + constructor( + @IConfigurationService private readonly configurationService: IConfigurationService, + @IRequestService private readonly requestService: IRequestService, + @IFileService private readonly fileService: IFileService, + @IProductService private readonly productService: IProductService, + @ILogService private readonly logService: ILogService, + ) { + super(); + } + + isEnabled(): boolean { + return this.getMcpGalleryUrl() !== undefined; + } + + async query(options?: IQueryOptions, token: CancellationToken = CancellationToken.None): Promise { + let result = await this.fetchGallery(token); + + if (options?.text) { + const searchText = options.text.toLowerCase(); + result = result.filter(item => item.name.toLowerCase().includes(searchText) || item.description.toLowerCase().includes(searchText)); + } + + const galleryServers: IGalleryMcpServer[] = []; + for (const item of result) { + galleryServers.push(this.toGalleryMcpServer(item)); + } + + return galleryServers; + } + + async getManifest(gallery: IGalleryMcpServer, token: CancellationToken): Promise { + const uri = URI.parse(gallery.manifestUrl); + if (uri.scheme === Schemas.file) { + try { + const content = await this.fileService.readFile(uri); + const data = content.value.toString(); + return JSON.parse(data); + } catch (error) { + this.logService.error(`Failed to read file from ${uri}: ${error}`); + } + } + + const context = await this.requestService.request({ + type: 'GET', + url: gallery.manifestUrl, + }, token); + + const result = await asJson(context); + if (!result) { + throw new Error(`Failed to fetch manifest from ${gallery.manifestUrl}`); + } + + return { + packages: result.packages, + remotes: result.remotes, + }; + } + + async getReadme(gallery: IGalleryMcpServer, token: CancellationToken): Promise { + const readmeUrl = gallery.readmeUrl; + if (!readmeUrl) { + return Promise.resolve(localize('noReadme', 'No README available')); + } + + const uri = URI.parse(readmeUrl); + if (uri.scheme === Schemas.file) { + try { + const content = await this.fileService.readFile(uri); + return content.value.toString(); + } catch (error) { + this.logService.error(`Failed to read file from ${uri}: ${error}`); + } + } + + const context = await this.requestService.request({ + type: 'GET', + url: readmeUrl, + }, token); + + const result = await asText(context); + if (!result) { + throw new Error(`Failed to fetch README from ${readmeUrl}`); + } + + return result; + } + + private toGalleryMcpServer(item: IRawGalleryMcpServer): IGalleryMcpServer { + let publisher = ''; + const nameParts = item.name.split('/'); + if (nameParts.length > 0) { + const domainParts = nameParts[0].split('.'); + if (domainParts.length > 0) { + publisher = domainParts[domainParts.length - 1]; // Always take the last part as owner + } + } + + return { + id: item.id, + name: item.name, + displayName: item.displayName ?? nameParts[nameParts.length - 1].split('-').map(s => uppercaseFirstLetter(s)).join(' '), + url: item.repository.url, + description: item.description, + version: item.version_detail.version, + lastUpdated: Date.parse(item.version_detail.release_date), + repositoryUrl: item.repository.url, + readmeUrl: item.readmeUrl, + manifestUrl: this.getManifestUrl(item), + packageTypes: item.package_types ?? [], + publisher, + publisherDisplayName: item.publisher?.displayName, + publisherDomain: item.publisher ? { + link: item.publisher.url, + verified: item.publisher.is_verified, + } : undefined, + }; + } + + private async fetchGallery(token: CancellationToken): Promise { + const mcpGalleryUrl = this.getMcpGalleryUrl(); + if (!mcpGalleryUrl) { + return Promise.resolve([]); + } + + const uri = URI.parse(mcpGalleryUrl); + if (uri.scheme === Schemas.file) { + try { + const content = await this.fileService.readFile(uri); + const data = content.value.toString(); + return JSON.parse(data); + } catch (error) { + this.logService.error(`Failed to read file from ${uri}: ${error}`); + } + } + + const context = await this.requestService.request({ + type: 'GET', + url: mcpGalleryUrl, + }, token); + + const result = await asJson(context); + return result || []; + } + + private getManifestUrl(item: IRawGalleryMcpServer): string { + const mcpGalleryUrl = this.getMcpGalleryUrl(); + + if (!mcpGalleryUrl) { + return item.repository.url; + } + + const uri = URI.parse(mcpGalleryUrl); + if (uri.scheme === Schemas.file) { + return joinPath(dirname(uri), item.id).fsPath; + } + + return `${mcpGalleryUrl}/${item.id}`; + } + + private getMcpGalleryUrl(): string | undefined { + if (this.productService.quality === 'stable') { + return undefined; + } + return this.configurationService.getValue(mcpGalleryServiceUrlConfig); + } + +} diff --git a/src/vs/platform/mcp/common/mcpManagement.ts b/src/vs/platform/mcp/common/mcpManagement.ts new file mode 100644 index 00000000000..fc72cd77735 --- /dev/null +++ b/src/vs/platform/mcp/common/mcpManagement.ts @@ -0,0 +1,172 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from '../../../base/common/cancellation.js'; +import { Event } from '../../../base/common/event.js'; +import { URI } from '../../../base/common/uri.js'; +import { SortBy, SortOrder } from '../../extensionManagement/common/extensionManagement.js'; +import { createDecorator } from '../../instantiation/common/instantiation.js'; +import { IMcpServerConfiguration } from './mcpPlatformTypes.js'; + +export interface ILocalMcpServer { + readonly name: string; + readonly config: IMcpServerConfiguration; + readonly version: string; + readonly location?: URI; + readonly id?: string; + readonly displayName?: string; + readonly url?: string; + readonly description?: string; + readonly repositoryUrl?: string; + readonly readmeUrl?: URI; + readonly publisher?: string; + readonly publisherDisplayName?: string; + readonly iconUrl?: string; + readonly manifest?: IMcpServerManifest; +} + +export interface IMcpServerInput { + readonly description?: string; + readonly is_required?: boolean; + readonly format?: 'string' | 'number' | 'boolean' | 'filepath'; + readonly value?: string; + readonly is_secret?: boolean; + readonly default?: string; + readonly choices?: readonly string[]; +} + +export interface IMcpServerVariableInput extends IMcpServerInput { + readonly variables?: Record; +} + +export interface IMcpServerPositionalArgument extends IMcpServerVariableInput { + readonly type: 'positional'; + readonly value_hint: string; + readonly is_repeatable: boolean; +} + +export interface IMcpServerNamedArgument extends IMcpServerVariableInput { + readonly type: 'named'; + readonly name: string; + readonly is_repeatable: boolean; +} + +export interface IMcpServerKeyValueInput extends IMcpServerVariableInput { + readonly name: string; + readonly value: string; +} + +export type IMcpServerArgument = IMcpServerPositionalArgument | IMcpServerNamedArgument; + +export const enum PackageType { + NODE = 'npm', + DOCKER = 'docker', + PYTHON = 'pypi', + REMOTE = 'remote', +} + +export interface IMcpServerPackage { + readonly name: string; + readonly version: string; + readonly registry_name: PackageType; + readonly package_arguments?: readonly IMcpServerArgument[]; + readonly runtime_arguments?: readonly IMcpServerArgument[]; + readonly environment_variables?: ReadonlyArray; +} + +export interface IMcpServerRemote { + readonly url: string; + readonly transport_type: 'streamable' | 'sse'; + readonly headers: ReadonlyArray; +} + +export interface IMcpServerManifest { + readonly packages: readonly IMcpServerPackage[]; + readonly remotes: readonly IMcpServerRemote[]; +} + +export interface IGalleryMcpServer { + readonly id: string; + readonly name: string; + readonly displayName: string; + readonly url: string; + readonly description: string; + readonly version: string; + readonly lastUpdated: number; + readonly repositoryUrl: string; + readonly manifestUrl: string; + readonly packageTypes: readonly PackageType[]; + readonly readmeUrl?: string; + readonly publisher: string; + readonly publisherDisplayName?: string; + readonly publisherDomain?: { link: string; verified: boolean }; + readonly iconUrl?: string; + readonly licenseUrl?: string; + readonly installCount?: number; + readonly rating?: number; + readonly ratingCount?: number; + readonly categories?: readonly string[]; + readonly tags?: readonly string[]; + readonly releaseDate?: number; +} + +export interface IQueryOptions { + text?: string; + sortBy?: SortBy; + sortOrder?: SortOrder; +} + +export interface InstallMcpServerEvent { + readonly name: string; + readonly source?: IGalleryMcpServer; + readonly applicationScoped?: boolean; + readonly workspaceScoped?: boolean; +} + +export interface InstallMcpServerResult { + readonly name: string; + readonly source?: IGalleryMcpServer; + readonly local?: ILocalMcpServer; + readonly error?: Error; + readonly applicationScoped?: boolean; + readonly workspaceScoped?: boolean; +} + +export interface UninstallMcpServerEvent { + readonly name: string; + readonly applicationScoped?: boolean; + readonly workspaceScoped?: boolean; +} + +export interface DidUninstallMcpServerEvent { + readonly name: string; + readonly error?: string; + readonly applicationScoped?: boolean; + readonly workspaceScoped?: boolean; +} + + +export const IMcpGalleryService = createDecorator('IMcpGalleryService'); +export interface IMcpGalleryService { + readonly _serviceBrand: undefined; + isEnabled(): boolean; + query(options?: IQueryOptions, token?: CancellationToken): Promise; + getManifest(extension: IGalleryMcpServer, token: CancellationToken): Promise; + getReadme(extension: IGalleryMcpServer, token: CancellationToken): Promise; +} + +export const IMcpManagementService = createDecorator('IMcpManagementService'); +export interface IMcpManagementService { + readonly _serviceBrand: undefined; + readonly onInstallMcpServer: Event; + readonly onDidInstallMcpServers: Event; + readonly onUninstallMcpServer: Event; + readonly onDidUninstallMcpServer: Event; + getInstalled(): Promise; + installFromGallery(server: IGalleryMcpServer, packageType: PackageType): Promise; + uninstall(server: ILocalMcpServer): Promise; +} + +export const mcpGalleryServiceUrlConfig = 'chat.mcp.gallery.serviceUrl'; diff --git a/src/vs/platform/mcp/common/mcpManagementService.ts b/src/vs/platform/mcp/common/mcpManagementService.ts new file mode 100644 index 00000000000..5d4f9031a13 --- /dev/null +++ b/src/vs/platform/mcp/common/mcpManagementService.ts @@ -0,0 +1,328 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { VSBuffer } from '../../../base/common/buffer.js'; +import { CancellationToken } from '../../../base/common/cancellation.js'; +import { Emitter } from '../../../base/common/event.js'; +import { Disposable } from '../../../base/common/lifecycle.js'; +import { deepClone } from '../../../base/common/objects.js'; +import { uppercaseFirstLetter } from '../../../base/common/strings.js'; +import { URI } from '../../../base/common/uri.js'; +import { ConfigurationTarget, IConfigurationService } from '../../configuration/common/configuration.js'; +import { IEnvironmentService } from '../../environment/common/environment.js'; +import { IFileService } from '../../files/common/files.js'; +import { ILogService } from '../../log/common/log.js'; +import { IUriIdentityService } from '../../uriIdentity/common/uriIdentity.js'; +import { DidUninstallMcpServerEvent, IGalleryMcpServer, ILocalMcpServer, IMcpGalleryService, IMcpManagementService, IMcpServerInput, IMcpServerManifest, InstallMcpServerEvent, InstallMcpServerResult, PackageType, UninstallMcpServerEvent } from './mcpManagement.js'; +import { McpConfigurationServer, IMcpServerVariable, McpServerVariableType, IMcpServersConfiguration, IMcpServerConfiguration } from './mcpPlatformTypes.js'; + +interface LocalMcpServer { + readonly name: string; + readonly version: string; + readonly id?: string; + readonly displayName?: string; + readonly url?: string; + readonly description?: string; + readonly repositoryUrl?: string; + readonly publisher?: string; + readonly publisherDisplayName?: string; + readonly iconUrl?: string; + readonly manifest?: IMcpServerManifest; +} + +export class McpManagementService extends Disposable implements IMcpManagementService { + + _serviceBrand: undefined; + + private readonly mcpLocation: URI; + + private readonly _onInstallMcpServer = this._register(new Emitter()); + readonly onInstallMcpServer = this._onInstallMcpServer.event; + + protected readonly _onDidInstallMcpServers = this._register(new Emitter()); + get onDidInstallMcpServers() { return this._onDidInstallMcpServers.event; } + + protected readonly _onUninstallMcpServer = this._register(new Emitter()); + get onUninstallMcpServer() { return this._onUninstallMcpServer.event; } + + protected _onDidUninstallMcpServer = this._register(new Emitter()); + get onDidUninstallMcpServer() { return this._onDidUninstallMcpServer.event; } + + constructor( + @IConfigurationService private readonly configurationService: IConfigurationService, + @IMcpGalleryService private readonly mcpGalleryService: IMcpGalleryService, + @IFileService private readonly fileService: IFileService, + @IEnvironmentService environmentService: IEnvironmentService, + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, + @ILogService private readonly logService: ILogService, + ) { + super(); + this.mcpLocation = uriIdentityService.extUri.joinPath(environmentService.userRoamingDataHome, 'mcp'); + } + + async getInstalled(): Promise { + const { userLocal } = this.configurationService.inspect('mcp'); + + if (!userLocal?.value?.servers) { + return []; + } + + return Promise.all(Object.entries(userLocal.value.servers).map(([name, config]) => this.scanServer(name, config))); + } + + private async scanServer(name: string, config: IMcpServerConfiguration): Promise { + let scanned: LocalMcpServer | undefined; + let readmeUrl: URI | undefined; + if (config.location) { + const manifestLocation = this.uriIdentityService.extUri.joinPath(URI.revive(config.location), 'manifest.json'); + try { + const content = await this.fileService.readFile(manifestLocation); + scanned = JSON.parse(content.value.toString()); + } catch (e) { + this.logService.error('MCP Management Service: failed to read manifest', config.location.toString(), e); + } + readmeUrl = this.uriIdentityService.extUri.joinPath(URI.revive(config.location), 'README.md'); + if (!await this.fileService.exists(readmeUrl)) { + readmeUrl = undefined; + } + } + + if (!scanned) { + let publisher = ''; + const nameParts = name.split('/'); + if (nameParts.length > 0) { + const domainParts = nameParts[0].split('.'); + if (domainParts.length > 0) { + publisher = domainParts[domainParts.length - 1]; // Always take the last part as owner + } + } + scanned = { + name, + version: '1.0.0', + displayName: nameParts[nameParts.length - 1].split('-').map(s => uppercaseFirstLetter(s)).join(' '), + publisher + }; + } + + return { + name, + config, + version: scanned.version, + location: URI.revive(config.location), + id: scanned.id, + displayName: scanned.displayName, + description: scanned.description, + publisher: scanned.publisher, + publisherDisplayName: scanned.publisherDisplayName, + repositoryUrl: scanned.repositoryUrl, + readmeUrl, + iconUrl: scanned.iconUrl, + manifest: scanned.manifest + }; + } + + async installFromGallery(server: IGalleryMcpServer, packageType: PackageType): Promise { + this.logService.trace('MCP Management Service: installGallery', server.url); + this._onInstallMcpServer.fire({ name: server.name }); + + try { + const manifest = await this.mcpGalleryService.getManifest(server, CancellationToken.None); + const location = this.uriIdentityService.extUri.joinPath(this.mcpLocation, `${server.name.replace('/', '.')}-${server.version}`); + const manifestPath = this.uriIdentityService.extUri.joinPath(location, 'manifest.json'); + await this.fileService.writeFile(manifestPath, VSBuffer.fromString(JSON.stringify({ + id: server.id, + name: server.name, + displayName: server.displayName, + description: server.description, + version: server.version, + publisher: server.publisher, + publisherDisplayName: server.publisherDisplayName, + repository: server.repositoryUrl, + licenseUrl: server.licenseUrl, + ...manifest, + }))); + + if (server.readmeUrl) { + const readme = await this.mcpGalleryService.getReadme(server, CancellationToken.None); + await this.fileService.writeFile(this.uriIdentityService.extUri.joinPath(location, 'README.md'), VSBuffer.fromString(readme)); + } + + const { userLocal } = this.configurationService.inspect('mcp'); + + const value: IMcpServersConfiguration = deepClone(userLocal?.value ?? { servers: {} }); + if (!value.servers) { + value.servers = {}; + } + const serverConfig = this.getServerConfig(manifest, packageType); + value.servers[server.name] = { + ...serverConfig, + location: location.toJSON(), + }; + if (serverConfig.inputs) { + value.inputs = value.inputs ?? []; + for (const input of serverConfig.inputs) { + if (!value.inputs.some(i => (i).id === input.id)) { + value.inputs.push({ ...input, serverName: server.name }); + } + } + } + + await this.configurationService.updateValue('mcp', value, ConfigurationTarget.USER_LOCAL); + const local = await this.scanServer(server.name, value.servers[server.name]); + this._onDidInstallMcpServers.fire([{ name: server.name, source: server, local }]); + } catch (e) { + this._onDidInstallMcpServers.fire([{ name: server.name, source: server, error: e }]); + throw e; + } + } + + async uninstall(server: ILocalMcpServer): Promise { + this.logService.trace('MCP Management Service: uninstall', server.name); + this._onUninstallMcpServer.fire({ name: server.name }); + + try { + const { userLocal } = this.configurationService.inspect('mcp'); + + const value: IMcpServersConfiguration = deepClone(userLocal?.value ?? { servers: {} }); + if (!value.servers) { + value.servers = {}; + } + delete value.servers[server.name]; + + if (value.inputs) { + const index = value.inputs.findIndex(i => (i).serverName === server.name); + if (index !== undefined && index >= 0) { + value.inputs?.splice(index, 1); + } + } + + await this.configurationService.updateValue('mcp', value, ConfigurationTarget.USER_LOCAL); + if (server.location) { + await this.fileService.del(URI.revive(server.location), { recursive: true }); + } + this._onDidUninstallMcpServer.fire({ name: server.name }); + } catch (e) { + this._onDidUninstallMcpServer.fire({ name: server.name, error: e }); + throw e; + } + } + + private getServerConfig(manifest: IMcpServerManifest, packageType: PackageType): McpConfigurationServer & { inputs?: IMcpServerVariable[] } { + if (packageType === PackageType.REMOTE) { + const inputs: IMcpServerVariable[] = []; + const headers: Record = {}; + for (const input of manifest.remotes[0].headers ?? []) { + headers[input.name] = input.value; + if (input.variables) { + inputs.push(...this.getVariables(input.variables)); + } + } + return { + type: 'http', + url: manifest.remotes[0].url, + headers: Object.keys(headers).length ? headers : undefined, + inputs: inputs.length ? inputs : undefined, + }; + } + + const serverPackage = manifest.packages.find(p => p.registry_name === packageType) ?? manifest.packages[0]; + const inputs: IMcpServerVariable[] = []; + const args: string[] = []; + const env: Record = {}; + + if (serverPackage.registry_name === PackageType.DOCKER) { + args.push('run'); + args.push('-i'); + args.push('--rm'); + } + + for (const arg of serverPackage.runtime_arguments ?? []) { + if (arg.type === 'positional') { + args.push(arg.value ?? arg.value_hint); + } else if (arg.type === 'named') { + args.push(arg.name); + if (arg.value) { + args.push(arg.value); + } + } + if (arg.variables) { + inputs.push(...this.getVariables(arg.variables)); + } + } + + for (const input of serverPackage.environment_variables ?? []) { + const variables = input.variables ? this.getVariables(input.variables) : []; + let value = input.value; + for (const variable of variables) { + value = value.replace(`{${variable.id}}`, `\${input:${variable.id}}`); + } + env[input.name] = value; + if (variables.length) { + inputs.push(...variables); + } + if (serverPackage.registry_name === PackageType.DOCKER) { + args.push('-e'); + args.push(input.name); + } + } + + if (serverPackage.registry_name === PackageType.NODE) { + args.push(`${serverPackage.name}@${serverPackage.version}`); + } + else if (serverPackage.registry_name === PackageType.PYTHON) { + args.push(`${serverPackage.name}==${serverPackage.version}`); + } + else if (serverPackage.registry_name === PackageType.DOCKER) { + args.push(`${serverPackage.name}:${serverPackage.version}`); + } + + for (const arg of serverPackage.package_arguments ?? []) { + if (arg.type === 'positional') { + args.push(arg.value ?? arg.value_hint); + } else if (arg.type === 'named') { + args.push(arg.name); + if (arg.value) { + args.push(arg.value); + } + } + if (arg.variables) { + inputs.push(...this.getVariables(arg.variables)); + } + } + + return { + type: 'stdio', + command: this.getCommandName(serverPackage.registry_name), + args: args.length ? args : undefined, + env: Object.keys(env).length ? env : undefined, + inputs: inputs.length ? inputs : undefined, + }; + } + + private getCommandName(packageType: PackageType): string { + switch (packageType) { + case PackageType.NODE: return 'npx'; + case PackageType.DOCKER: return 'docker'; + case PackageType.PYTHON: return 'uvx'; + } + return packageType; + } + + private getVariables(variableInputs: Record): IMcpServerVariable[] { + const variables: IMcpServerVariable[] = []; + for (const [key, value] of Object.entries(variableInputs)) { + variables.push({ + id: key, + type: value.choices ? McpServerVariableType.PICK : McpServerVariableType.PROMPT, + description: value.description ?? '', + password: !!value.is_secret, + default: value.default, + options: value.choices, + }); + } + return variables; + } + +} diff --git a/src/vs/platform/mcp/common/mcpPlatformTypes.ts b/src/vs/platform/mcp/common/mcpPlatformTypes.ts index 078c70306cf..87e01b16eeb 100644 --- a/src/vs/platform/mcp/common/mcpPlatformTypes.ts +++ b/src/vs/platform/mcp/common/mcpPlatformTypes.ts @@ -3,6 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { IStringDictionary } from '../../../base/common/collections.js'; +import { UriComponents } from '../../../base/common/uri.js'; + export interface IMcpConfiguration { inputs?: unknown[]; /** @deprecated Only for rough cross-compat with other formats */ @@ -25,3 +28,41 @@ export interface IMcpConfigurationHTTP { url: string; headers?: Record; } + +export const enum McpServerVariableType { + PROMPT = 'promptString', + PICK = 'pickString', +} + +export interface IMcpServerVariable { + readonly id: string; + readonly type: McpServerVariableType; + readonly description: string; + readonly password: boolean; + readonly default?: string; + readonly options?: readonly string[]; + readonly serverName?: string; +} + +export interface IMcpServerConfiguration { + readonly location?: UriComponents; +} + +export interface IMcpStdioServerConfiguration extends IMcpServerConfiguration { + readonly type: 'stdio'; + readonly command: string; + readonly args?: readonly string[]; + readonly env?: Record; + readonly envFile?: string; +} + +export interface IMcpRemtoeServerConfiguration extends IMcpServerConfiguration { + readonly type: 'http'; + readonly url: string; + readonly headers?: Record; +} + +export interface IMcpServersConfiguration { + servers?: IStringDictionary; + inputs?: IMcpServerVariable[]; +} diff --git a/src/vs/platform/menubar/electron-main/menubar.ts b/src/vs/platform/menubar/electron-main/menubar.ts index 40682438abb..ec817068b7a 100644 --- a/src/vs/platform/menubar/electron-main/menubar.ts +++ b/src/vs/platform/menubar/electron-main/menubar.ts @@ -288,7 +288,7 @@ export class Menubar extends Disposable { const dockMenu = new Menu(); dockMenu.append(new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'miNewWindow', comment: ['&& denotes a mnemonic'] }, "New &&Window")), click: () => this.windowsMainService.openEmptyWindow({ context: OpenContext.DOCK }) })); - app.dock.setMenu(dockMenu); + app.dock!.setMenu(dockMenu); } // File diff --git a/src/vs/platform/native/common/native.ts b/src/vs/platform/native/common/native.ts index 88b12468e52..2905c0b324a 100644 --- a/src/vs/platform/native/common/native.ts +++ b/src/vs/platform/native/common/native.ts @@ -38,6 +38,28 @@ export interface INativeHostOptions { readonly targetWindowId?: number; } +export const enum FocusMode { + + /** + * (Default) Transfer focus to the target window + * when the editor is focused. + */ + Transfer, + + /** + * Transfer focus to the target window when the + * editor is focused, otherwise notify the user that + * the app has activity (macOS/Windows only). + */ + Notify, + + /** + * Force the window to be focused, even if the editor + * is not currently focused. + */ + Force, +} + export interface ICommonNativeHostService { readonly _serviceBrand: undefined; @@ -110,13 +132,10 @@ export interface ICommonNativeHostService { /** * Make the window focused. - * - * @param options Pass `force: true` if you want to make the window take - * focus even if the application does not have focus currently. This option - * should only be used if it is necessary to steal focus from the current - * focused application which may not be VSCode. + * @param options specify the specific window to focus and the focus mode. + * Defaults to {@link FocusMode.Transfer}. */ - focusWindow(options?: INativeHostOptions & { force?: boolean }): Promise; + focusWindow(options?: INativeHostOptions & { mode?: FocusMode }): Promise; // Dialogs showMessageBox(options: MessageBoxOptions & INativeHostOptions): Promise; diff --git a/src/vs/platform/native/electron-main/nativeHostMainService.ts b/src/vs/platform/native/electron-main/nativeHostMainService.ts index e60c3d6d465..25b65672fda 100644 --- a/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -28,7 +28,7 @@ import { IEnvironmentMainService } from '../../environment/electron-main/environ import { createDecorator, IInstantiationService } from '../../instantiation/common/instantiation.js'; import { ILifecycleMainService, IRelaunchOptions } from '../../lifecycle/electron-main/lifecycleMainService.js'; import { ILogService } from '../../log/common/log.js'; -import { ICommonNativeHostService, INativeHostOptions, IOSProperties, IOSStatistics } from '../common/native.js'; +import { FocusMode, ICommonNativeHostService, INativeHostOptions, IOSProperties, IOSStatistics } from '../common/native.js'; import { IProductService } from '../../product/common/productService.js'; import { IPartsSplash } from '../../theme/common/themeService.js'; import { IThemeMainService } from '../../theme/electron-main/themeMainService.js'; @@ -344,9 +344,9 @@ export class NativeHostMainService extends Disposable implements INativeHostMain window?.updateWindowControls(options); } - async focusWindow(windowId: number | undefined, options?: INativeHostOptions & { force?: boolean }): Promise { + async focusWindow(windowId: number | undefined, options?: INativeHostOptions & { mode?: FocusMode }): Promise { const window = this.windowById(options?.targetWindowId, windowId); - window?.focus({ force: options?.force ?? false }); + window?.focus({ mode: options?.mode ?? FocusMode.Transfer }); } async setMinimumSize(windowId: number | undefined, width: number | undefined, height: number | undefined): Promise { diff --git a/src/vs/platform/notification/common/notification.ts b/src/vs/platform/notification/common/notification.ts index a3b59ab038e..f1101520856 100644 --- a/src/vs/platform/notification/common/notification.ts +++ b/src/vs/platform/notification/common/notification.ts @@ -371,16 +371,6 @@ export interface INotificationService { readonly _serviceBrand: undefined; - /** - * Emitted when a new notification is added. - */ - readonly onDidAddNotification: Event; - - /** - * Emitted when a notification is removed. - */ - readonly onDidRemoveNotification: Event; - /** * Emitted when the notifications filter changed. */ diff --git a/src/vs/platform/notification/test/common/testNotificationService.ts b/src/vs/platform/notification/test/common/testNotificationService.ts index 80def17f244..bb388543c3a 100644 --- a/src/vs/platform/notification/test/common/testNotificationService.ts +++ b/src/vs/platform/notification/test/common/testNotificationService.ts @@ -8,10 +8,6 @@ import { INotification, INotificationHandle, INotificationService, INotification export class TestNotificationService implements INotificationService { - readonly onDidAddNotification: Event = Event.None; - - readonly onDidRemoveNotification: Event = Event.None; - readonly onDidChangeFilter: Event = Event.None; declare readonly _serviceBrand: undefined; diff --git a/src/vs/platform/prompts/test/common/utils/mock.ts b/src/vs/platform/prompts/test/common/utils/mock.ts index 3771b085c2e..7f94641ab61 100644 --- a/src/vs/platform/prompts/test/common/utils/mock.ts +++ b/src/vs/platform/prompts/test/common/utils/mock.ts @@ -26,7 +26,7 @@ export function mockObject( } } - const service: object = new Proxy( + const mocked: object = new Proxy( {}, { get: ( @@ -42,15 +42,13 @@ export function mockObject( // note! it's ok to type assert here, because of the explicit runtime // assertion above - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions return overrides[key as T] as TObject[T]; }, }); // note! it's ok to type assert here, because of the runtime checks in // the `Proxy` getter - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - return service as TObject; + return mocked as TObject; } /** diff --git a/src/vs/platform/prompts/tsconfig.strict.json b/src/vs/platform/prompts/tsconfig.strict.json deleted file mode 100644 index 27429134795..00000000000 --- a/src/vs/platform/prompts/tsconfig.strict.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "extends": "../../../tsconfig.json", - "compilerOptions": { - "strict": true, - "allowUnusedLabels": false, - "allowUnreachableCode": false, - "alwaysStrict": true, - "exactOptionalPropertyTypes": true, - "noFallthroughCasesInSwitch": true, - "noImplicitAny": true, - "noImplicitOverride": true, - "noImplicitReturns": true, - "noImplicitThis": true, - "noPropertyAccessFromIndexSignature": true, - "noUncheckedIndexedAccess": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - } -} diff --git a/src/vs/platform/terminal/node/terminalProfiles.ts b/src/vs/platform/terminal/node/terminalProfiles.ts index fd5d14a71fb..948a8b350ce 100644 --- a/src/vs/platform/terminal/node/terminalProfiles.ts +++ b/src/vs/platform/terminal/node/terminalProfiles.ts @@ -85,11 +85,7 @@ async function detectAvailableWindowsProfiles( const is32ProcessOn64Windows = process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432'); const system32Path = `${process.env['windir']}\\${is32ProcessOn64Windows ? 'Sysnative' : 'System32'}`; - let useWSLexe = false; - - if (getWindowsBuildNumber() >= 16299) { - useWSLexe = true; - } + const useWSLexe = getWindowsBuildNumber() >= 22000; await initializeWindowsProfiles(testPwshSourcePaths); diff --git a/src/vs/platform/userDataSync/common/userDataProfilesManifestSync.ts b/src/vs/platform/userDataSync/common/userDataProfilesManifestSync.ts index 438b76f47e0..3314bafeb98 100644 --- a/src/vs/platform/userDataSync/common/userDataProfilesManifestSync.ts +++ b/src/vs/platform/userDataSync/common/userDataProfilesManifestSync.ts @@ -15,7 +15,7 @@ import { IUriIdentityService } from '../../uriIdentity/common/uriIdentity.js'; import { IUserDataProfile, IUserDataProfilesService } from '../../userDataProfile/common/userDataProfile.js'; import { AbstractSynchroniser, IAcceptResult, IMergeResult, IResourcePreview } from './abstractSynchronizer.js'; import { merge } from './userDataProfilesManifestMerge.js'; -import { Change, IRemoteUserData, IUserDataSyncLocalStoreService, IUserDataSynchroniser, IUserDataSyncLogService, IUserDataSyncEnablementService, IUserDataSyncStoreService, SyncResource, USER_DATA_SYNC_SCHEME, ISyncUserDataProfile, ISyncData, IUserDataResourceManifest, UserDataSyncError, UserDataSyncErrorCode } from './userDataSync.js'; +import { Change, IRemoteUserData, ISyncData, ISyncUserDataProfile, IUserDataResourceManifest, IUserDataSyncEnablementService, IUserDataSynchroniser, IUserDataSyncLocalStoreService, IUserDataSyncLogService, IUserDataSyncStoreService, SyncResource, USER_DATA_SYNC_SCHEME, UserDataSyncError, UserDataSyncErrorCode } from './userDataSync.js'; interface IUserDataProfileManifestResourceMergeResult extends IAcceptResult { readonly local: { added: ISyncUserDataProfile[]; removed: IUserDataProfile[]; updated: ISyncUserDataProfile[] }; @@ -213,6 +213,7 @@ export class UserDataProfilesManifestSynchroniser extends AbstractSynchroniser i if (canAddRemoteProfiles) { for (const profile of remote?.added || []) { const collection = await this.userDataSyncStoreService.createCollection(this.syncHeaders); + this.logService.trace(`${this.syncResourceLogLabel}: Created collection "${collection}" for "${profile.name}".`); addedCollections.push(collection); remoteProfiles.push({ id: profile.id, name: profile.name, collection, icon: profile.icon, useDefaultFlags: profile.useDefaultFlags }); } diff --git a/src/vs/platform/webContentExtractor/electron-main/webContentExtractorService.ts b/src/vs/platform/webContentExtractor/electron-main/webContentExtractorService.ts index bd5adb42daf..af054374ee5 100644 --- a/src/vs/platform/webContentExtractor/electron-main/webContentExtractorService.ts +++ b/src/vs/platform/webContentExtractor/electron-main/webContentExtractorService.ts @@ -50,7 +50,7 @@ export class NativeWebContentExtractorService implements IWebContentExtractorSer height: 600, show: false, webPreferences: { - javascript: false, + javascript: true, offscreen: true, sandbox: true, webgl: false diff --git a/src/vs/platform/window/electron-main/window.ts b/src/vs/platform/window/electron-main/window.ts index 11c60408036..bfd4632e83d 100644 --- a/src/vs/platform/window/electron-main/window.ts +++ b/src/vs/platform/window/electron-main/window.ts @@ -9,9 +9,10 @@ import { Event } from '../../../base/common/event.js'; import { IDisposable } from '../../../base/common/lifecycle.js'; import { ISerializableCommandAction } from '../../action/common/action.js'; import { NativeParsedArgs } from '../../environment/common/argv.js'; +import { FocusMode } from '../../native/common/native.js'; import { IUserDataProfile } from '../../userDataProfile/common/userDataProfile.js'; -import { DEFAULT_AUX_WINDOW_SIZE, DEFAULT_WINDOW_SIZE, INativeWindowConfiguration } from '../common/window.js'; import { ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier } from '../../workspace/common/workspace.js'; +import { DEFAULT_AUX_WINDOW_SIZE, DEFAULT_WINDOW_SIZE, INativeWindowConfiguration } from '../common/window.js'; export interface IBaseWindow extends IDisposable { @@ -26,7 +27,7 @@ export interface IBaseWindow extends IDisposable { readonly win: electron.BrowserWindow | null; readonly lastFocusTime: number; - focus(options?: { force: boolean }): void; + focus(options?: { mode: FocusMode }): void; setRepresentedFilename(name: string): void; getRepresentedFilename(): string | undefined; diff --git a/src/vs/platform/windows/electron-main/windowImpl.ts b/src/vs/platform/windows/electron-main/windowImpl.ts index fa41388269e..1e031a1f571 100644 --- a/src/vs/platform/windows/electron-main/windowImpl.ts +++ b/src/vs/platform/windows/electron-main/windowImpl.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import electron, { BrowserWindowConstructorOptions } from 'electron'; +import electron, { BrowserWindowConstructorOptions, Display, screen } from 'electron'; import { DeferredPromise, RunOnceScheduler, timeout, Delayer } from '../../../base/common/async.js'; import { CancellationToken } from '../../../base/common/cancellation.js'; import { toErrorMessage } from '../../../base/common/errorMessage.js'; @@ -45,6 +45,7 @@ import { ILoggerMainService } from '../../log/electron-main/loggerService.js'; import { IInstantiationService } from '../../instantiation/common/instantiation.js'; import { VSBuffer } from '../../../base/common/buffer.js'; import { errorHandler } from '../../../base/common/errors.js'; +import { FocusMode } from '../../native/common/native.js'; export interface IWindowCreationOptions { readonly state: IWindowState; @@ -115,14 +116,34 @@ export abstract class BaseWindow extends Disposable implements IBaseWindow { protected _lastFocusTime = Date.now(); // window is shown on creation so take current time get lastFocusTime(): number { return this._lastFocusTime; } + private maximizedWindowState: IWindowState | undefined; + protected _win: electron.BrowserWindow | null = null; get win() { return this._win; } protected setWin(win: electron.BrowserWindow, options?: BrowserWindowConstructorOptions): void { this._win = win; // Window Events - this._register(Event.fromNodeEventEmitter(win, 'maximize')(() => this._onDidMaximize.fire())); - this._register(Event.fromNodeEventEmitter(win, 'unmaximize')(() => this._onDidUnmaximize.fire())); + this._register(Event.fromNodeEventEmitter(win, 'maximize')(() => { + if (isWindows && this.environmentMainService.enableRDPDisplayTracking && this._win) { + const [x, y] = this._win.getPosition(); + const [width, height] = this._win.getSize(); + + this.maximizedWindowState = { mode: WindowMode.Maximized, width, height, x, y }; + this.logService.debug(`Saved maximized window ${this.id} display state:`, this.maximizedWindowState); + } + + this._onDidMaximize.fire(); + })); + this._register(Event.fromNodeEventEmitter(win, 'unmaximize')(() => { + if (isWindows && this.environmentMainService.enableRDPDisplayTracking && this.maximizedWindowState) { + this.maximizedWindowState = undefined; + + this.logService.debug(`Cleared maximized window ${this.id} state`); + } + + this._onDidUnmaximize.fire(); + })); this._register(Event.fromNodeEventEmitter(win, 'closed')(() => { this._onDidClose.fire(); @@ -197,6 +218,24 @@ export abstract class BaseWindow extends Disposable implements IBaseWindow { this.joinNativeFullScreenTransition?.complete(true); })); } + + if (isWindows && this.environmentMainService.enableRDPDisplayTracking) { + // Handles the display-added event on Windows RDP multi-monitor scenarios. + // This helps restore maximized windows to their correct monitor after RDP reconnection. + // Refs https://github.com/electron/electron/issues/47016 + this._register(Event.fromNodeEventEmitter(screen, 'display-added', (event: Electron.Event, display: Display) => ({ event, display }))((e) => { + this.onDisplayAdded(e.display); + })); + } + } + + private onDisplayAdded(display: Display): void { + const state = this.maximizedWindowState; + if (state && this._win && WindowStateValidator.validateWindowStateOnDisplay(state, display)) { + this.logService.debug(`Setting maximized window ${this.id} bounds to match newly added display`, state); + + this._win.setBounds(state); + } } constructor( @@ -287,11 +326,32 @@ export abstract class BaseWindow extends Disposable implements IBaseWindow { return !!this.documentEdited; } - focus(options?: { force: boolean }): void { - if (isMacintosh && options?.force) { - electron.app.focus({ steal: true }); - } + focus(options?: { mode: FocusMode }): void { + switch (options?.mode ?? FocusMode.Transfer) { + case FocusMode.Transfer: + this.doFocusWindow(); + break; + case FocusMode.Notify: + if (isMacintosh) { + electron.app.dock?.bounce('informational'); + } else if (isWindows) { + // On Windows, this just flashes the taskbar icon, which is desired + // https://github.com/electron/electron/issues/2867 + this.win?.focus(); + } + break; + + case FocusMode.Force: + if (isMacintosh) { + electron.app.focus({ steal: true }); + } + this.doFocusWindow(); + break; + } + } + + private doFocusWindow() { const win = this.win; if (!win) { return; @@ -1054,7 +1114,7 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { this._register(new RunOnceScheduler(() => { if (this._win && !this._win.isVisible() && !this._win.isMinimized()) { this._win.show(); - this.focus({ force: true }); + this.focus({ mode: FocusMode.Force }); this._win.webContents.openDevTools(); } }, 10000)).schedule(); diff --git a/src/vs/platform/windows/electron-main/windows.ts b/src/vs/platform/windows/electron-main/windows.ts index 42be1e253a5..8fb315a001d 100644 --- a/src/vs/platform/windows/electron-main/windows.ts +++ b/src/vs/platform/windows/electron-main/windows.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import electron from 'electron'; +import electron, { Display, Rectangle } from 'electron'; import { Color } from '../../../base/common/color.js'; import { Event } from '../../../base/common/event.js'; import { join } from '../../../base/common/path.js'; @@ -350,14 +350,7 @@ export namespace WindowStateValidator { logService.error('window#validateWindowState: error finding display for window state', error); } - if ( - display && // we have a display matching the desired bounds - displayWorkingArea && // we have valid working area bounds - state.x + state.width > displayWorkingArea.x && // prevent window from falling out of the screen to the left - state.y + state.height > displayWorkingArea.y && // prevent window from falling out of the screen to the top - state.x < displayWorkingArea.x + displayWorkingArea.width && // prevent window from falling out of the screen to the right - state.y < displayWorkingArea.y + displayWorkingArea.height // prevent window from falling out of the screen to the bottom - ) { + if (display && validateWindowStateOnDisplay(state, display)) { return state; } @@ -366,6 +359,27 @@ export namespace WindowStateValidator { return undefined; } + export function validateWindowStateOnDisplay(state: IWindowState, display: Display): state is Rectangle { + if ( + typeof state.x !== 'number' || + typeof state.y !== 'number' || + typeof state.width !== 'number' || + typeof state.height !== 'number' || + state.width <= 0 || state.height <= 0 + ) { + return false; + } + + const displayWorkingArea = getWorkingArea(display); + return Boolean( + displayWorkingArea && // we have valid working area bounds + state.x + state.width > displayWorkingArea.x && // prevent window from falling out of the screen to the left + state.y + state.height > displayWorkingArea.y && // prevent window from falling out of the screen to the top + state.x < displayWorkingArea.x + displayWorkingArea.width && // prevent window from falling out of the screen to the right + state.y < displayWorkingArea.y + displayWorkingArea.height // prevent window from falling out of the screen to the bottom + ); + } + function getWorkingArea(display: electron.Display): electron.Rectangle | undefined { // Prefer the working area of the display to account for taskbars on the diff --git a/src/vs/platform/windows/test/electron-main/windowsFinder.test.ts b/src/vs/platform/windows/test/electron-main/windowsFinder.test.ts index c0889db322b..0dcfde67393 100644 --- a/src/vs/platform/windows/test/electron-main/windowsFinder.test.ts +++ b/src/vs/platform/windows/test/electron-main/windowsFinder.test.ts @@ -18,6 +18,7 @@ import { toWorkspaceFolders } from '../../../workspaces/common/workspaces.js'; import { IWorkspaceIdentifier } from '../../../workspace/common/workspace.js'; import { FileAccess } from '../../../../base/common/network.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; +import { FocusMode } from '../../../native/common/native.js'; suite('WindowsFinder', () => { @@ -60,7 +61,7 @@ suite('WindowsFinder', () => { addTabbedWindow(window: ICodeWindow): void { throw new Error('Method not implemented.'); } load(config: INativeWindowConfiguration, options: { isReload?: boolean }): void { throw new Error('Method not implemented.'); } reload(cli?: NativeParsedArgs): void { throw new Error('Method not implemented.'); } - focus(options?: { force: boolean }): void { throw new Error('Method not implemented.'); } + focus(options?: { mode: FocusMode }): void { throw new Error('Method not implemented.'); } close(): void { throw new Error('Method not implemented.'); } getBounds(): Electron.Rectangle { throw new Error('Method not implemented.'); } send(channel: string, ...args: any[]): void { throw new Error('Method not implemented.'); } diff --git a/src/vs/workbench/api/browser/mainThreadAuthentication.ts b/src/vs/workbench/api/browser/mainThreadAuthentication.ts index 64413947986..db00ad2aa60 100644 --- a/src/vs/workbench/api/browser/mainThreadAuthentication.ts +++ b/src/vs/workbench/api/browser/mainThreadAuthentication.ts @@ -34,6 +34,7 @@ export interface AuthenticationGetSessionOptions { forceNewSession?: boolean | AuthenticationInteractiveOptions; silent?: boolean; account?: AuthenticationSessionAccount; + issuer?: UriComponents; } export class MainThreadAuthenticationProvider extends Disposable implements IAuthenticationProvider { @@ -45,6 +46,7 @@ export class MainThreadAuthenticationProvider extends Disposable implements IAut public readonly id: string, public readonly label: string, public readonly supportsMultipleAccounts: boolean, + public readonly issuers: ReadonlyArray, private readonly notificationService: INotificationService, onDidChangeSessionsEmitter: Emitter, ) { @@ -98,7 +100,7 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu })); } - async $registerAuthenticationProvider(id: string, label: string, supportsMultipleAccounts: boolean): Promise { + async $registerAuthenticationProvider(id: string, label: string, supportsMultipleAccounts: boolean, supportedIssuers: UriComponents[] = []): Promise { if (!this.authenticationService.declaredProviders.find(p => p.id === id)) { // If telemetry shows that this is not happening much, we can instead throw an error here. this.logService.warn(`Authentication provider ${id} was not declared in the Extension Manifest.`); @@ -111,7 +113,8 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu } const emitter = new Emitter(); this._registrations.set(id, emitter); - const provider = new MainThreadAuthenticationProvider(this._proxy, id, label, supportsMultipleAccounts, this.notificationService, emitter); + const supportedIssuerUris = supportedIssuers.map(i => URI.revive(i)); + const provider = new MainThreadAuthenticationProvider(this._proxy, id, label, supportsMultipleAccounts, supportedIssuerUris, this.notificationService, emitter); this.authenticationService.registerAuthenticationProvider(id, provider); } @@ -203,7 +206,8 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu } private async doGetSession(providerId: string, scopes: string[], extensionId: string, extensionName: string, options: AuthenticationGetSessionOptions): Promise { - const sessions = await this.authenticationService.getSessions(providerId, scopes, options.account, true); + const issuer = URI.revive(options.issuer); + const sessions = await this.authenticationService.getSessions(providerId, scopes, options.account, true, issuer); const provider = this.authenticationService.getProvider(providerId); // Error cases @@ -268,7 +272,14 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu } else { const accountToCreate: AuthenticationSessionAccount | undefined = options.account ?? matchingAccountPreferenceSession?.account; do { - session = await this.authenticationService.createSession(providerId, scopes, { activateImmediate: true, account: accountToCreate }); + session = await this.authenticationService.createSession( + providerId, + scopes, + { + activateImmediate: true, + account: accountToCreate, + issuer + }); } while ( accountToCreate && accountToCreate.label !== session.account.label diff --git a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index 4560cc6026f..4be5c86b6d4 100644 --- a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -29,7 +29,7 @@ import { IChatAgentHistoryEntry, IChatAgentImplementation, IChatAgentRequest, IC import { IChatEditingService, IChatRelatedFileProviderMetadata } from '../../contrib/chat/common/chatEditingService.js'; import { ChatRequestAgentPart } from '../../contrib/chat/common/chatParserTypes.js'; import { ChatRequestParser } from '../../contrib/chat/common/chatRequestParser.js'; -import { IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatNotebookEdit, IChatProgress, IChatService, IChatTask, IChatWarningMessage } from '../../contrib/chat/common/chatService.js'; +import { IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatNotebookEdit, IChatProgress, IChatService, IChatTask, IChatTaskSerialized, IChatWarningMessage } from '../../contrib/chat/common/chatService.js'; import { ChatAgentLocation, ChatMode } from '../../contrib/chat/common/constants.js'; import { IExtHostContext, extHostNamedCustomer } from '../../services/extensions/common/extHostCustomers.js'; import { IExtensionService } from '../../services/extensions/common/extensions.js'; @@ -72,6 +72,14 @@ export class MainThreadChatTask implements IChatTask { this.progress.push(progress); this._onDidAddProgress.fire(progress); } + + toJSON(): IChatTaskSerialized { + return { + kind: 'progressTaskSerialized', + content: this.content, + progress: this.progress + }; + } } @extHostNamedCustomer(MainContext.MainThreadChatAgents2) @@ -180,9 +188,6 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA provideChatTitle: (history, token) => { return this._proxy.$provideChatTitle(handle, history, token); }, - provideSampleQuestions: (location: ChatAgentLocation, token: CancellationToken) => { - return this._proxy.$provideSampleQuestions(handle, location, token); - } }; let disposable: IDisposable; diff --git a/src/vs/workbench/api/browser/mainThreadDecorations.ts b/src/vs/workbench/api/browser/mainThreadDecorations.ts index 9363c8bb9a4..ef271b163d9 100644 --- a/src/vs/workbench/api/browser/mainThreadDecorations.ts +++ b/src/vs/workbench/api/browser/mainThreadDecorations.ts @@ -10,12 +10,14 @@ import { ExtHostContext, MainContext, MainThreadDecorationsShape, ExtHostDecorat import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js'; import { IDecorationsService, IDecorationData } from '../../services/decorations/common/decorations.js'; import { CancellationToken } from '../../../base/common/cancellation.js'; +import { DeferredPromise } from '../../../base/common/async.js'; +import { CancellationError } from '../../../base/common/errors.js'; class DecorationRequestsQueue { private _idPool = 0; private _requests = new Map(); - private _resolver = new Map any>(); + private _resolver = new Map>(); private _timer: any; @@ -28,16 +30,18 @@ class DecorationRequestsQueue { enqueue(uri: URI, token: CancellationToken): Promise { const id = ++this._idPool; - const result = new Promise(resolve => { - this._requests.set(id, { id, uri }); - this._resolver.set(id, resolve); - this._processQueue(); - }); + + const defer = new DeferredPromise(); + this._requests.set(id, { id, uri }); + this._resolver.set(id, defer); + this._processQueue(); + const sub = token.onCancellationRequested(() => { this._requests.delete(id); this._resolver.delete(id); + defer.error(new CancellationError()); }); - return result.finally(() => sub.dispose()); + return defer.p.finally(() => sub.dispose()); } private _processQueue(): void { @@ -50,8 +54,8 @@ class DecorationRequestsQueue { const requests = this._requests; const resolver = this._resolver; this._proxy.$provideDecorations(this._handle, [...requests.values()], CancellationToken.None).then(data => { - for (const [id, resolve] of resolver) { - resolve(data[id]); + for (const [id, defer] of resolver) { + defer.complete(data[id]); } }); diff --git a/src/vs/workbench/api/browser/mainThreadLanguageModelTools.ts b/src/vs/workbench/api/browser/mainThreadLanguageModelTools.ts index 8944b82e633..cd3a304fb4c 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageModelTools.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageModelTools.ts @@ -6,10 +6,10 @@ import { CancellationToken } from '../../../base/common/cancellation.js'; import { Disposable, DisposableMap } from '../../../base/common/lifecycle.js'; import { revive } from '../../../base/common/marshalling.js'; -import { CountTokensCallback, ILanguageModelToolsService, IToolData, IToolInvocation, IToolResult, ToolProgress, toolResultHasBuffers, IToolProgressStep } from '../../contrib/chat/common/languageModelToolsService.js'; +import { CountTokensCallback, ILanguageModelToolsService, IToolInvocation, IToolProgressStep, IToolResult, ToolProgress, toolResultHasBuffers } from '../../contrib/chat/common/languageModelToolsService.js'; import { IExtHostContext, extHostNamedCustomer } from '../../services/extensions/common/extHostCustomers.js'; import { Dto, SerializableObjectWithBuffers } from '../../services/extensions/common/proxyIdentifier.js'; -import { ExtHostContext, ExtHostLanguageModelToolsShape, MainContext, MainThreadLanguageModelToolsShape } from '../common/extHost.protocol.js'; +import { ExtHostContext, ExtHostLanguageModelToolsShape, IToolDataDto, MainContext, MainThreadLanguageModelToolsShape } from '../common/extHost.protocol.js'; @extHostNamedCustomer(MainContext.MainThreadLanguageModelTools) export class MainThreadLanguageModelTools extends Disposable implements MainThreadLanguageModelToolsShape { @@ -28,11 +28,24 @@ export class MainThreadLanguageModelTools extends Disposable implements MainThre super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostLanguageModelTools); - this._register(this._languageModelToolsService.onDidChangeTools(e => this._proxy.$onDidChangeTools([...this._languageModelToolsService.getTools()]))); + this._register(this._languageModelToolsService.onDidChangeTools(e => this._proxy.$onDidChangeTools(this.getToolDtos()))); } - async $getTools(): Promise { - return Array.from(this._languageModelToolsService.getTools()); + private getToolDtos(): IToolDataDto[] { + return Array.from(this._languageModelToolsService.getTools()) + .map(tool => ({ + id: tool.id, + displayName: tool.displayName, + toolReferenceName: tool.toolReferenceName, + tags: tool.tags, + userDescription: tool.userDescription, + modelDescription: tool.modelDescription, + inputSchema: tool.inputSchema, + } satisfies IToolDataDto)); + } + + async $getTools(): Promise { + return this.getToolDtos(); } async $invokeTool(dto: IToolInvocation, token?: CancellationToken): Promise | SerializableObjectWithBuffers>> { diff --git a/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts b/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts index 8c38e14647e..2a7e0a513e3 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts @@ -15,7 +15,7 @@ import { NotebookDto } from './mainThreadNotebookDto.js'; import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js'; import { INotebookEditor } from '../../contrib/notebook/browser/notebookBrowser.js'; import { INotebookEditorService } from '../../contrib/notebook/browser/services/notebookEditorService.js'; -import { INotebookCellExecution, INotebookExecution, INotebookExecutionStateService, NotebookExecutionType } from '../../contrib/notebook/common/notebookExecutionStateService.js'; +import { INotebookCellExecution, INotebookExecution, INotebookExecutionStateService } from '../../contrib/notebook/common/notebookExecutionStateService.js'; import { IKernelSourceActionProvider, INotebookKernel, INotebookKernelChangeEvent, INotebookKernelDetectionTask, INotebookKernelService, VariablesResult } from '../../contrib/notebook/common/notebookKernelService.js'; import { SerializableObjectWithBuffers } from '../../services/extensions/common/proxyIdentifier.js'; import { ExtHostContext, ExtHostNotebookKernelsShape, ICellExecuteUpdateDto, ICellExecutionCompleteDto, INotebookKernelDto2, MainContext, MainThreadNotebookKernelsShape } from '../common/extHost.protocol.js'; @@ -146,12 +146,6 @@ export class MainThreadNotebookKernels implements MainThreadNotebookKernelsShape this._notebookExecutions.forEach(e => e.complete()); })); - this._disposables.add(this._notebookExecutionStateService.onDidChangeExecution(e => { - if (e.type === NotebookExecutionType.cell) { - this._proxy.$cellExecutionChanged(e.notebook, e.cellHandle, e.changed?.state); - } - })); - this._disposables.add(this._notebookKernelService.onDidChangeSelectedNotebooks(e => { for (const [handle, [kernel,]] of this._kernels) { if (e.oldKernel === kernel.id) { diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 1f8c97d2642..9c65df21dca 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -298,6 +298,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I ) { checkProposedApiEnabled(extension, 'authLearnMore'); } + if (options?.issuer) { + checkProposedApiEnabled(extension, 'authIssuers'); + } return extHostAuthentication.getSession(extension, providerId, scopes, options as any); }, getAccounts(providerId: string) { @@ -312,6 +315,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return _asExtensionEvent(extHostAuthentication.getExtensionScopedSessionsEvent(extension.identifier.value)); }, registerAuthenticationProvider(id: string, label: string, provider: vscode.AuthenticationProvider, options?: vscode.AuthenticationProviderOptions): vscode.Disposable { + if (options?.supportedIssuers) { + checkProposedApiEnabled(extension, 'authIssuers'); + } return extHostAuthentication.registerAuthenticationProvider(id, label, provider, options); } }; @@ -1391,10 +1397,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'notebookKernelSource'); return extHostNotebookKernels.registerKernelSourceActionProvider(extension, notebookType, provider); }, - onDidChangeNotebookCellExecutionState(listener, thisArgs?, disposables?) { - checkProposedApiEnabled(extension, 'notebookCellExecutionState'); - return _asExtensionEvent(extHostNotebookKernels.onDidChangeNotebookCellExecutionState)(listener, thisArgs, disposables); - } }; // namespace: l10n @@ -1804,6 +1806,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I ChatResponseConfirmationPart: extHostTypes.ChatResponseConfirmationPart, ChatResponseMovePart: extHostTypes.ChatResponseMovePart, ChatResponseExtensionsPart: extHostTypes.ChatResponseExtensionsPart, + ChatPrepareToolInvocationPart: extHostTypes.ChatPrepareToolInvocationPart, ChatResponseReferencePartStatusKind: extHostTypes.ChatResponseReferencePartStatusKind, ChatRequestTurn: extHostTypes.ChatRequestTurn, ChatRequestTurn2: extHostTypes.ChatRequestTurn, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index cd387657765..0d175b9d310 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -9,6 +9,7 @@ import { IRemoteConsoleLog } from '../../../base/common/console.js'; import { SerializedError } from '../../../base/common/errors.js'; import { IRelativePattern } from '../../../base/common/glob.js'; import { IMarkdownString } from '../../../base/common/htmlContent.js'; +import { IJSONSchema } from '../../../base/common/jsonSchema.js'; import { IDisposable } from '../../../base/common/lifecycle.js'; import * as performance from '../../../base/common/performance.js'; import Severity from '../../../base/common/severity.js'; @@ -59,7 +60,7 @@ import { IChatContentInlineReference, IChatFollowup, IChatNotebookEdit, IChatPro import { IChatRequestVariableValue } from '../../contrib/chat/common/chatVariables.js'; import { ChatAgentLocation } from '../../contrib/chat/common/constants.js'; import { IChatMessage, IChatResponseFragment, ILanguageModelChatMetadata, ILanguageModelChatSelector, ILanguageModelsChangeEvent } from '../../contrib/chat/common/languageModels.js'; -import { IPreparedToolInvocation, IToolData, IToolInvocation, IToolProgressStep, IToolResult } from '../../contrib/chat/common/languageModelToolsService.js'; +import { IPreparedToolInvocation, IToolInvocation, IToolProgressStep, IToolResult } from '../../contrib/chat/common/languageModelToolsService.js'; import { DebugConfigurationProviderTriggerKind, IAdapterDescriptor, IConfig, IDebugSessionReplMode, IDebugTestRunReference, IDebugVisualization, IDebugVisualizationContext, IDebugVisualizationTreeItem, MainThreadDebugVisualization } from '../../contrib/debug/common/debug.js'; import { McpCollectionDefinition, McpConnectionState, McpServerDefinition, McpServerLaunch } from '../../contrib/mcp/common/mcpTypes.js'; import * as notebookCommon from '../../contrib/notebook/common/notebookCommon.js'; @@ -74,6 +75,7 @@ import { CoverageDetails, ExtensionRunTestsRequest, ICallProfileRunHandler, IFil import { Timeline, TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor } from '../../contrib/timeline/common/timeline.js'; import { TypeHierarchyItem } from '../../contrib/typeHierarchy/common/typeHierarchy.js'; import { RelatedInformationResult, RelatedInformationType } from '../../services/aiRelatedInformation/common/aiRelatedInformation.js'; +import { AiSettingsSearchProviderOptions, AiSettingsSearchResult } from '../../services/aiSettingsSearch/common/aiSettingsSearch.js'; import { AuthenticationSession, AuthenticationSessionAccount, AuthenticationSessionsChangeEvent, IAuthenticationCreateSessionOptions, IAuthenticationProviderSessionOptions } from '../../services/authentication/common/authentication.js'; import { EditorGroupColumn } from '../../services/editor/common/editorGroupColumn.js'; import { IExtensionDescriptionDelta, IStaticWorkspaceData } from '../../services/extensions/common/extensionHostProtocol.js'; @@ -86,7 +88,6 @@ import { CandidatePort } from '../../services/remote/common/tunnelModel.js'; import { IFileQueryBuilderOptions, ITextQueryBuilderOptions } from '../../services/search/common/queryBuilder.js'; import * as search from '../../services/search/common/search.js'; import { AISearchKeyword, TextSearchCompleteMessage } from '../../services/search/common/searchExtTypes.js'; -import { AiSettingsSearchProviderOptions, AiSettingsSearchResult } from '../../services/aiSettingsSearch/common/aiSettingsSearch.js'; import { ISaveProfileResult } from '../../services/userDataProfile/common/userDataProfile.js'; import { TerminalShellExecutionCommandLineConfidence } from './extHostTypes.js'; import * as tasks from './shared/tasks.js'; @@ -180,7 +181,7 @@ export interface AuthenticationGetSessionOptions { } export interface MainThreadAuthenticationShape extends IDisposable { - $registerAuthenticationProvider(id: string, label: string, supportsMultipleAccounts: boolean): void; + $registerAuthenticationProvider(id: string, label: string, supportsMultipleAccounts: boolean, supportedIssuers?: UriComponents[]): void; $unregisterAuthenticationProvider(id: string): void; $ensureProvider(id: string): Promise; $sendDidChangeSessions(providerId: string, event: AuthenticationSessionsChangeEvent): void; @@ -1361,7 +1362,6 @@ export interface ExtHostChatAgentsShape2 { $acceptAction(handle: number, result: IChatAgentResult, action: IChatUserActionEvent): void; $invokeCompletionProvider(handle: number, query: string, token: CancellationToken): Promise; $provideChatTitle(handle: number, context: IChatAgentHistoryEntryDto[], token: CancellationToken): Promise; - $provideSampleQuestions(handle: number, location: ChatAgentLocation, token: CancellationToken): Promise; $releaseSession(sessionId: string): void; $detectChatParticipant(handle: number, request: Dto, context: { history: IChatAgentHistoryEntryDto[] }, options: { participants: IChatParticipantMetadata[]; location: ChatAgentLocation }, token: CancellationToken): Promise; $provideRelatedFiles(handle: number, request: Dto, token: CancellationToken): Promise[] | undefined>; @@ -1377,7 +1377,15 @@ export interface IChatParticipantDetectionResult { command?: string; } -export type IToolDataDto = Omit; +export interface IToolDataDto { + id: string; + toolReferenceName?: string; + tags?: string[]; + displayName: string; + userDescription?: string; + modelDescription: string; + inputSchema?: IJSONSchema; +} export interface MainThreadLanguageModelToolsShape extends IDisposable { $getTools(): Promise[]>; @@ -2900,7 +2908,6 @@ export interface ExtHostNotebookKernelsShape { $executeCells(handle: number, uri: UriComponents, handles: number[]): Promise; $cancelCells(handle: number, uri: UriComponents, handles: number[]): Promise; $acceptKernelMessageFromRenderer(handle: number, editorId: string, message: any): void; - $cellExecutionChanged(uri: UriComponents, cellHandle: number, state: notebookCommon.NotebookCellExecutionState | undefined): void; $provideKernelSourceActions(handle: number, token: CancellationToken): Promise; $provideVariables(handle: number, requestId: string, notebookUri: UriComponents, parentId: number | undefined, kind: 'named' | 'indexed', start: number, token: CancellationToken): Promise; } diff --git a/src/vs/workbench/api/common/extHostApiCommands.ts b/src/vs/workbench/api/common/extHostApiCommands.ts index 0dc498638b9..436e3505751 100644 --- a/src/vs/workbench/api/common/extHostApiCommands.ts +++ b/src/vs/workbench/api/common/extHostApiCommands.ts @@ -60,7 +60,7 @@ const newCommands: ApiCommand[] = [ range!: vscode.Range; selectionRange!: vscode.Range; children!: vscode.DocumentSymbol[]; - override containerName!: string; + override containerName: string = ''; } return value.map(MergedInfo.to); diff --git a/src/vs/workbench/api/common/extHostAuthentication.ts b/src/vs/workbench/api/common/extHostAuthentication.ts index e705e38df50..ee09391811b 100644 --- a/src/vs/workbench/api/common/extHostAuthentication.ts +++ b/src/vs/workbench/api/common/extHostAuthentication.ts @@ -11,6 +11,7 @@ import { IExtensionDescription, ExtensionIdentifier } from '../../../platform/ex import { INTERNAL_AUTH_PROVIDER_PREFIX } from '../../services/authentication/common/authentication.js'; import { createDecorator } from '../../../platform/instantiation/common/instantiation.js'; import { IExtHostRpcService } from './extHostRpcService.js'; +import { URI } from '../../../base/common/uri.js'; export interface IExtHostAuthentication extends ExtHostAuthentication { } export const IExtHostAuthentication = createDecorator('IExtHostAuthentication'); @@ -88,7 +89,7 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { this._authenticationProviders.set(id, { label, provider, options: options ?? { supportsMultipleAccounts: false } }); const listener = provider.onDidChangeSessions(e => this._proxy.$sendDidChangeSessions(id, e)); - this._proxy.$registerAuthenticationProvider(id, label, options?.supportsMultipleAccounts ?? false); + this._proxy.$registerAuthenticationProvider(id, label, options?.supportsMultipleAccounts ?? false, options?.supportedIssuers); return new Disposable(() => { listener.dispose(); @@ -100,6 +101,7 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { async $createSession(providerId: string, scopes: string[], options: vscode.AuthenticationProviderSessionOptions): Promise { const providerData = this._authenticationProviders.get(providerId); if (providerData) { + options.issuer = URI.revive(options.issuer); return await providerData.provider.createSession(scopes, options); } @@ -118,6 +120,7 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { async $getSessions(providerId: string, scopes: ReadonlyArray | undefined, options: vscode.AuthenticationProviderSessionOptions): Promise> { const providerData = this._authenticationProviders.get(providerId); if (providerData) { + options.issuer = URI.revive(options.issuer); return await providerData.provider.getSessions(scopes, options); } diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 1c83abfbf26..a39b50b2c65 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -13,7 +13,7 @@ import { Iterable } from '../../../base/common/iterator.js'; import { Disposable, DisposableMap, DisposableStore, toDisposable } from '../../../base/common/lifecycle.js'; import { revive } from '../../../base/common/marshalling.js'; import { StopWatch } from '../../../base/common/stopwatch.js'; -import { assertType } from '../../../base/common/types.js'; +import { assertType, isDefined } from '../../../base/common/types.js'; import { URI } from '../../../base/common/uri.js'; import { generateUuid } from '../../../base/common/uuid.js'; import { Location } from '../../../editor/common/languages.js'; @@ -244,6 +244,15 @@ class ChatAgentResponseStream { _report(dto); return this; }, + prepareToolInvocation(toolName) { + throwIfDone(this.prepareToolInvocation); + checkProposedApiEnabled(that._extension, 'chatParticipantAdditions'); + + const part = new extHostTypes.ChatPrepareToolInvocationPart(toolName); + const dto = typeConvert.ChatPrepareToolInvocationPart.from(part); + _report(dto); + return this; + }, push(part) { throwIfDone(this.push); @@ -285,6 +294,11 @@ class ChatAgentResponseStream { that._sessionDisposables.add(toDisposable(() => cts.dispose(true))); } _report(dto); + } else if (part instanceof extHostTypes.ChatPrepareToolInvocationPart) { + checkProposedApiEnabled(that._extension, 'chatParticipantAdditions'); + const dto = typeConvert.ChatPrepareToolInvocationPart.from(part); + _report(dto); + return this; } else { const dto = typeConvert.ChatResponsePart.from(part, that._commandsConverter, that._sessionDisposables); _report(dto); @@ -410,7 +424,15 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS const { request, location, history } = await this._createRequest(requestDto, context, detector.extension); const model = await this.getModelForRequest(request, detector.extension); - const extRequest = typeConvert.ChatAgentRequest.to(request, location, model, this.getDiagnosticsWhenEnabled(detector.extension), this.getToolsForRequest(detector.extension, request), this.getTools2ForRequest(detector.extension, request), detector.extension); + const extRequest = typeConvert.ChatAgentRequest.to( + request, + location, + model, + this.getDiagnosticsWhenEnabled(detector.extension), + this.getToolsForRequest(detector.extension, request), + this.getTools2ForRequest(detector.extension, request), + detector.extension, + this._logService); return detector.provider.provideParticipantDetection( extRequest, @@ -501,7 +523,8 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS this.getDiagnosticsWhenEnabled(agent.extension), this.getToolsForRequest(agent.extension, request), this.getTools2ForRequest(agent.extension, request), - agent.extension + agent.extension, + this._logService ); inFlightRequest = { requestId: requestDto.requestId, extRequest }; this._inFlightRequests.add(inFlightRequest); @@ -598,7 +621,8 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS // REQUEST turn const varsWithoutTools = h.request.variables.variables .filter(v => v.kind !== 'tool') - .map(v => typeConvert.ChatPromptReference.to(v, this.getDiagnosticsWhenEnabled(extension))); + .map(v => typeConvert.ChatPromptReference.to(v, this.getDiagnosticsWhenEnabled(extension), this._logService)) + .filter(isDefined); const toolReferences = h.request.variables.variables .filter(v => v.kind === 'tool') .map(typeConvert.ChatLanguageModelToolReference.to); @@ -713,16 +737,6 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS const history = await this.prepareHistoryTurns(agent.extension, agent.id, { history: context }); return await agent.provideTitle({ history }, token); } - - async $provideSampleQuestions(handle: number, location: ChatAgentLocation, token: CancellationToken): Promise { - const agent = this._agents.get(handle); - if (!agent) { - return; - } - - return (await agent.provideSampleQuestions(typeConvert.ChatLocation.to(location), token)) - .map(f => typeConvert.ChatFollowup.from(f, undefined)); - } } class ExtHostParticipantDetector { @@ -750,7 +764,6 @@ class ExtHostChatAgent { private _onDidPerformAction = new Emitter(); private _supportIssueReporting: boolean | undefined; private _agentVariableProvider?: { provider: vscode.ChatParticipantCompletionItemProvider; triggerCharacters: string[] }; - private _welcomeMessageProvider?: vscode.ChatWelcomeMessageProvider | undefined; private _additionalWelcomeMessage?: string | vscode.MarkdownString | undefined; private _titleProvider?: vscode.ChatTitleProvider | undefined; private _requester: vscode.ChatRequesterInformation | undefined; @@ -808,18 +821,6 @@ class ExtHostChatAgent { return await this._titleProvider.provideChatTitle(context, token) ?? undefined; } - async provideSampleQuestions(location: vscode.ChatLocation, token: CancellationToken): Promise { - if (!this._welcomeMessageProvider || !this._welcomeMessageProvider.provideSampleQuestions) { - return []; - } - const content = await this._welcomeMessageProvider.provideSampleQuestions(location, token); - if (!content) { - return []; - } - - return content; - } - get apiAgent(): vscode.ChatParticipant { let disposed = false; let updateScheduled = false; @@ -935,15 +936,6 @@ class ExtHostChatAgent { checkProposedApiEnabled(that.extension, 'chatParticipantAdditions'); return that._agentVariableProvider; }, - set welcomeMessageProvider(v) { - checkProposedApiEnabled(that.extension, 'defaultChatParticipant'); - that._welcomeMessageProvider = v; - updateMetadataSoon(); - }, - get welcomeMessageProvider() { - checkProposedApiEnabled(that.extension, 'defaultChatParticipant'); - return that._welcomeMessageProvider; - }, set additionalWelcomeMessage(v) { checkProposedApiEnabled(that.extension, 'defaultChatParticipant'); that._additionalWelcomeMessage = v; diff --git a/src/vs/workbench/api/common/extHostDebugService.ts b/src/vs/workbench/api/common/extHostDebugService.ts index 46648099c38..ced3a2ea275 100644 --- a/src/vs/workbench/api/common/extHostDebugService.ts +++ b/src/vs/workbench/api/common/extHostDebugService.ts @@ -63,7 +63,7 @@ export interface IExtHostDebugService extends ExtHostDebugServiceShape { export abstract class ExtHostDebugServiceBase extends DisposableCls implements IExtHostDebugService, ExtHostDebugServiceShape { - readonly _serviceBrand: undefined; + declare readonly _serviceBrand: undefined; private _configProviderHandleCounter: number; private _configProviders: ConfigProviderTuple[]; diff --git a/src/vs/workbench/api/common/extHostFileSystemEventService.ts b/src/vs/workbench/api/common/extHostFileSystemEventService.ts index 5fcd3c613d0..3fbd84d3392 100644 --- a/src/vs/workbench/api/common/extHostFileSystemEventService.ts +++ b/src/vs/workbench/api/common/extHostFileSystemEventService.ts @@ -14,7 +14,7 @@ import { Disposable, WorkspaceEdit } from './extHostTypes.js'; import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; import { FileChangeFilter, FileOperation, IGlobPatterns } from '../../../platform/files/common/files.js'; import { CancellationToken } from '../../../base/common/cancellation.js'; -import { ILogService } from '../../../platform/log/common/log.js'; +import { ILogService, LogLevel } from '../../../platform/log/common/log.js'; import { IExtHostWorkspace } from './extHostWorkspace.js'; import { Lazy } from '../../../base/common/lazy.js'; import { ExtHostConfigProvider } from './extHostConfiguration.js'; @@ -29,6 +29,9 @@ export interface FileSystemWatcherCreateOptions { class FileSystemWatcher implements vscode.FileSystemWatcher { + private static IDS = 0; + + private readonly id = FileSystemWatcher.IDS++; private readonly session = Math.random(); private readonly _onDidCreate = new Emitter(); @@ -50,7 +53,16 @@ class FileSystemWatcher implements vscode.FileSystemWatcher { return Boolean(this._config & 0b100); } - constructor(mainContext: IMainContext, configuration: ExtHostConfigProvider, workspace: IExtHostWorkspace, extension: IExtensionDescription, dispatcher: Event, globPattern: string | IRelativePatternDto, options: FileSystemWatcherCreateOptions) { + constructor( + mainContext: IMainContext, + logService: ILogService, + configuration: ExtHostConfigProvider, + workspace: IExtHostWorkspace, + extension: IExtensionDescription, + dispatcher: Event, + globPattern: string | IRelativePatternDto, + options: FileSystemWatcherCreateOptions + ) { this._config = 0; if (options.ignoreCreateEvents) { this._config += 0b001; @@ -62,6 +74,18 @@ class FileSystemWatcher implements vscode.FileSystemWatcher { this._config += 0b100; } + const trace = logService.getLevel() === LogLevel.Trace; + if (trace) { + let patternLogMsg: string; + if (typeof globPattern === 'string') { + patternLogMsg = `'${globPattern}'`; + } else { + patternLogMsg = `base: '${globPattern.base}', pattern: '${globPattern.pattern}'`; + } + + logService.trace(`[File Watcher ('API') ${this.id}] createFileSystemWatcher(${patternLogMsg}, ${JSON.stringify(options)})`); + } + const parsedPattern = parse(globPattern); // 1.64.x behaviour change: given the new support to watch any folder @@ -81,34 +105,64 @@ class FileSystemWatcher implements vscode.FileSystemWatcher { const subscription = dispatcher(events => { if (typeof events.session === 'number' && events.session !== this.session) { + if (trace) { + logService.trace(`[File Watcher ('API') ${this.id}] dispatch(): returning early due to event correlation mismatch`); + } return; // ignore events from other file watchers that are in correlation mode } if (excludeUncorrelatedEvents && typeof events.session === 'undefined') { + if (trace) { + logService.trace(`[File Watcher ('API') ${this.id}] dispatch(): returning early due to event correlation mismatch`); + } return; // ignore events from other non-correlating file watcher when we are in correlation mode } if (!options.ignoreCreateEvents) { for (const created of events.created) { const uri = URI.revive(created); - if (parsedPattern(uri.fsPath) && (!excludeOutOfWorkspaceEvents || workspace.getWorkspaceFolder(uri))) { - this._onDidCreate.fire(uri); + if (parsedPattern(uri.fsPath)) { + if (!excludeOutOfWorkspaceEvents || workspace.getWorkspaceFolder(uri)) { + this._onDidCreate.fire(uri); + } else { + logService.trace(`[File Watcher ('API') ${this.id}] dispatch(created): ${uri.fsPath} did not match out-of-workspace rule`); + } + } else { + if (trace) { + logService.trace(`[File Watcher ('API') ${this.id}] dispatch(created): ${uri.fsPath} did not match pattern`); + } } } } if (!options.ignoreChangeEvents) { for (const changed of events.changed) { const uri = URI.revive(changed); - if (parsedPattern(uri.fsPath) && (!excludeOutOfWorkspaceEvents || workspace.getWorkspaceFolder(uri))) { - this._onDidChange.fire(uri); + if (parsedPattern(uri.fsPath)) { + if (!excludeOutOfWorkspaceEvents || workspace.getWorkspaceFolder(uri)) { + this._onDidChange.fire(uri); + } else { + logService.trace(`[File Watcher ('API') ${this.id}] dispatch(changed): ${uri.fsPath} did not match out-of-workspace rule`); + } + } else { + if (trace) { + logService.trace(`[File Watcher ('API') ${this.id}] dispatch(changed): ${uri.fsPath} did not match pattern`); + } } } } if (!options.ignoreDeleteEvents) { for (const deleted of events.deleted) { const uri = URI.revive(deleted); - if (parsedPattern(uri.fsPath) && (!excludeOutOfWorkspaceEvents || workspace.getWorkspaceFolder(uri))) { - this._onDidDelete.fire(uri); + if (parsedPattern(uri.fsPath)) { + if (!excludeOutOfWorkspaceEvents || workspace.getWorkspaceFolder(uri)) { + this._onDidDelete.fire(uri); + } else { + logService.trace(`[File Watcher ('API') ${this.id}] dispatch(deleted): ${uri.fsPath} did not match out-of-workspace rule`); + } + } else { + if (trace) { + logService.trace(`[File Watcher ('API') ${this.id}] dispatch(deleted): ${uri.fsPath} did not match pattern`); + } } } } @@ -285,7 +339,7 @@ export class ExtHostFileSystemEventService implements ExtHostFileSystemEventServ //--- file events createFileSystemWatcher(workspace: IExtHostWorkspace, configProvider: ExtHostConfigProvider, extension: IExtensionDescription, globPattern: vscode.GlobPattern, options: FileSystemWatcherCreateOptions): vscode.FileSystemWatcher { - return new FileSystemWatcher(this._mainContext, configProvider, workspace, extension, this._onFileSystemEvent.event, typeConverter.GlobPattern.from(globPattern), options); + return new FileSystemWatcher(this._mainContext, this._logService, configProvider, workspace, extension, this._onFileSystemEvent.event, typeConverter.GlobPattern.from(globPattern), options); } $onFileEvent(events: FileSystemEvents) { diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index 9993c5c2112..407e22259c3 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -1335,7 +1335,7 @@ class InlineCompletionAdapter { items: readonly vscode.InlineCompletionItem[]; }>(); - private readonly _isAdditionsProposedApiEnabled = isProposedApiEnabled(this._extension, 'inlineCompletionsAdditions'); + private readonly _isAdditionsProposedApiEnabled: boolean; constructor( private readonly _extension: IExtensionDescription, @@ -1343,6 +1343,7 @@ class InlineCompletionAdapter { private readonly _provider: vscode.InlineCompletionItemProvider, private readonly _commands: CommandsConverter, ) { + this._isAdditionsProposedApiEnabled = isProposedApiEnabled(this._extension, 'inlineCompletionsAdditions'); } public get supportsHandleEvents(): boolean { diff --git a/src/vs/workbench/api/common/extHostNotebookKernels.ts b/src/vs/workbench/api/common/extHostNotebookKernels.ts index 1994bc803b5..d96cbc78588 100644 --- a/src/vs/workbench/api/common/extHostNotebookKernels.ts +++ b/src/vs/workbench/api/common/extHostNotebookKernels.ts @@ -18,9 +18,9 @@ import { IExtHostInitDataService } from './extHostInitDataService.js'; import { ExtHostNotebookController } from './extHostNotebook.js'; import { ExtHostCell, ExtHostNotebookDocument } from './extHostNotebookDocument.js'; import * as extHostTypeConverters from './extHostTypeConverters.js'; -import { NotebookCellExecutionState as ExtHostNotebookCellExecutionState, NotebookCellOutput, NotebookControllerAffinity2, NotebookVariablesRequestKind } from './extHostTypes.js'; +import { NotebookCellOutput, NotebookControllerAffinity2, NotebookVariablesRequestKind } from './extHostTypes.js'; import { asWebviewUri } from '../../contrib/webview/common/webview.js'; -import { INotebookKernelSourceAction, NotebookCellExecutionState } from '../../contrib/notebook/common/notebookCommon.js'; +import { INotebookKernelSourceAction } from '../../contrib/notebook/common/notebookCommon.js'; import { CellExecutionUpdateType } from '../../contrib/notebook/common/notebookExecutionService.js'; import { checkProposedApiEnabled } from '../../services/extensions/common/extensions.js'; import { SerializableObjectWithBuffers } from '../../services/extensions/common/proxyIdentifier.js'; @@ -55,9 +55,6 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { private readonly _kernelData = new Map(); private _handlePool: number = 0; - private readonly _onDidChangeCellExecutionState = new Emitter(); - readonly onDidChangeNotebookCellExecutionState = this._onDidChangeCellExecutionState.event; - constructor( mainContext: IMainContext, private readonly _initData: IExtHostInitDataService, @@ -511,19 +508,6 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { obj.onDidReceiveMessage.fire(Object.freeze({ editor: editor.apiEditor, message })); } - $cellExecutionChanged(uri: UriComponents, cellHandle: number, state: NotebookCellExecutionState | undefined): void { - const document = this._extHostNotebook.getNotebookDocument(URI.revive(uri)); - const cell = document.getCell(cellHandle); - if (cell) { - const newState = state ? extHostTypeConverters.NotebookCellExecutionState.to(state) : ExtHostNotebookCellExecutionState.Idle; - if (newState !== undefined) { - this._onDidChangeCellExecutionState.fire({ - cell: cell.apiCell, - state: newState - }); - } - } - } // --- diff --git a/src/vs/workbench/api/common/extHostSearch.ts b/src/vs/workbench/api/common/extHostSearch.ts index 92ba9241bf8..4dc474ba575 100644 --- a/src/vs/workbench/api/common/extHostSearch.ts +++ b/src/vs/workbench/api/common/extHostSearch.ts @@ -31,25 +31,35 @@ export const IExtHostSearch = createDecorator('IExtHostSearch'); export class ExtHostSearch implements IExtHostSearch { - protected readonly _proxy: MainThreadSearchShape = this.extHostRpc.getProxy(MainContext.MainThreadSearch); - protected _handlePool: number = 0; + protected readonly _proxy: MainThreadSearchShape; + protected _handlePool: number; - private readonly _textSearchProvider = new Map(); - private readonly _textSearchUsedSchemes = new Set(); + private readonly _textSearchProvider: Map; + private readonly _textSearchUsedSchemes: Set; - private readonly _aiTextSearchProvider = new Map(); - private readonly _aiTextSearchUsedSchemes = new Set(); + private readonly _aiTextSearchProvider: Map; + private readonly _aiTextSearchUsedSchemes: Set; - private readonly _fileSearchProvider = new Map(); - private readonly _fileSearchUsedSchemes = new Set(); + private readonly _fileSearchProvider: Map; + private readonly _fileSearchUsedSchemes: Set; - private readonly _fileSearchManager = new FileSearchManager(); + private readonly _fileSearchManager: FileSearchManager; constructor( @IExtHostRpcService private extHostRpc: IExtHostRpcService, @IURITransformerService protected _uriTransformer: IURITransformerService, @ILogService protected _logService: ILogService, - ) { } + ) { + this._proxy = this.extHostRpc.getProxy(MainContext.MainThreadSearch); + this._handlePool = 0; + this._textSearchProvider = new Map(); + this._textSearchUsedSchemes = new Set(); + this._aiTextSearchProvider = new Map(); + this._aiTextSearchUsedSchemes = new Set(); + this._fileSearchProvider = new Map(); + this._fileSearchUsedSchemes = new Set(); + this._fileSearchManager = new FileSearchManager(); + } protected _transformScheme(scheme: string): string { return this._uriTransformer.transformOutgoingScheme(scheme); @@ -219,4 +229,3 @@ export function reviveQuery(rawQuery: U): U extends IRawTex function reviveFolderQuery(rawFolderQuery: IFolderQuery): IFolderQuery { return revive(rawFolderQuery); } - diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 6a964ad5d4b..04c15d85b85 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -42,7 +42,7 @@ import { IViewBadge } from '../../common/views.js'; import { IChatAgentRequest, IChatAgentResult } from '../../contrib/chat/common/chatAgents.js'; import { IChatRequestDraft } from '../../contrib/chat/common/chatEditingService.js'; import { IChatRequestVariableEntry, isImageVariableEntry } from '../../contrib/chat/common/chatModel.js'; -import { IChatAgentMarkdownContentWithVulnerability, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatExtensionsContent, IChatFollowup, IChatMarkdownContent, IChatMoveMessage, IChatProgressMessage, IChatResponseCodeblockUriPart, IChatTaskDto, IChatTaskResult, IChatTextEdit, IChatTreeData, IChatUserActionEvent, IChatWarningMessage } from '../../contrib/chat/common/chatService.js'; +import { IChatAgentMarkdownContentWithVulnerability, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatExtensionsContent, IChatFollowup, IChatMarkdownContent, IChatMoveMessage, IChatPrepareToolInvocationPart, IChatProgressMessage, IChatResponseCodeblockUriPart, IChatTaskDto, IChatTaskResult, IChatTextEdit, IChatTreeData, IChatUserActionEvent, IChatWarningMessage } from '../../contrib/chat/common/chatService.js'; import { IToolData, IToolResult } from '../../contrib/chat/common/languageModelToolsService.js'; import * as chatProvider from '../../contrib/chat/common/languageModels.js'; import { IChatMessageDataPart, IChatResponseDataPart, IChatResponsePromptTsxPart, IChatResponseTextPart } from '../../contrib/chat/common/languageModels.js'; @@ -65,6 +65,7 @@ import { LanguageModelDataPart, LanguageModelPromptTsxPart, LanguageModelTextPar import { ChatAgentLocation } from '../../contrib/chat/common/constants.js'; import { AiSettingsSearchResult, AiSettingsSearchResultKind } from '../../services/aiSettingsSearch/common/aiSettingsSearch.js'; import { McpServerLaunch, McpServerTransportType } from '../../contrib/mcp/common/mcpTypes.js'; +import { ILogService } from '../../../platform/log/common/log.js'; export namespace Command { @@ -1657,21 +1658,6 @@ export namespace NotebookCellExecutionSummary { } } -export namespace NotebookCellExecutionState { - export function to(state: notebooks.NotebookCellExecutionState): vscode.NotebookCellExecutionState | undefined { - if (state === notebooks.NotebookCellExecutionState.Unconfirmed) { - return types.NotebookCellExecutionState.Pending; - } else if (state === notebooks.NotebookCellExecutionState.Pending) { - // Since the (proposed) extension API doesn't have the distinction between Unconfirmed and Pending, we don't want to fire an update for Pending twice - return undefined; - } else if (state === notebooks.NotebookCellExecutionState.Executing) { - return types.NotebookCellExecutionState.Executing; - } else { - throw new Error(`Unknown state: ${state}`); - } - } -} - export namespace NotebookCellKind { export function from(data: vscode.NotebookCellKind): notebooks.CellKind { switch (data) { @@ -2708,6 +2694,19 @@ export namespace ChatResponseMovePart { } } +export namespace ChatPrepareToolInvocationPart { + export function from(part: vscode.ChatPrepareToolInvocationPart): IChatPrepareToolInvocationPart { + return { + kind: 'prepareToolInvocation', + toolName: part.toolName, + }; + } + + export function to(part: IChatPrepareToolInvocationPart): vscode.ChatPrepareToolInvocationPart { + return new types.ChatPrepareToolInvocationPart(part.toolName); + } +} + export namespace ChatTask { export function from(part: vscode.ChatResponseProgressPart2): IChatTaskDto { return { @@ -2854,7 +2853,7 @@ export namespace ChatResponseCodeCitationPart { export namespace ChatResponsePart { - export function from(part: vscode.ChatResponsePart | vscode.ChatResponseTextEditPart | vscode.ChatResponseMarkdownWithVulnerabilitiesPart | vscode.ChatResponseWarningPart | vscode.ChatResponseConfirmationPart | vscode.ChatResponseReferencePart2 | vscode.ChatResponseMovePart | vscode.ChatResponseNotebookEditPart | vscode.ChatResponseExtensionsPart, commandsConverter: CommandsConverter, commandDisposables: DisposableStore): extHostProtocol.IChatProgressDto { + export function from(part: vscode.ExtendedChatResponsePart, commandsConverter: CommandsConverter, commandDisposables: DisposableStore): extHostProtocol.IChatProgressDto { if (part instanceof types.ChatResponseMarkdownPart) { return ChatResponseMarkdownPart.from(part); } else if (part instanceof types.ChatResponseAnchorPart) { @@ -2885,6 +2884,8 @@ export namespace ChatResponsePart { return ChatResponseMovePart.from(part); } else if (part instanceof types.ChatResponseExtensionsPart) { return ChatResponseExtensionsPart.from(part); + } else if (part instanceof types.ChatPrepareToolInvocationPart) { + return ChatPrepareToolInvocationPart.from(part); } return { @@ -2920,7 +2921,7 @@ export namespace ChatResponsePart { } export namespace ChatAgentRequest { - export function to(request: IChatAgentRequest, location2: vscode.ChatRequestEditorData | vscode.ChatRequestNotebookData | undefined, model: vscode.LanguageModelChat, diagnostics: readonly [vscode.Uri, readonly vscode.Diagnostic[]][], toolSelection: vscode.ChatRequestToolSelection | undefined, tools: Map, extension: IRelaxedExtensionDescription): vscode.ChatRequest { + export function to(request: IChatAgentRequest, location2: vscode.ChatRequestEditorData | vscode.ChatRequestNotebookData | undefined, model: vscode.LanguageModelChat, diagnostics: readonly [vscode.Uri, readonly vscode.Diagnostic[]][], toolSelection: vscode.ChatRequestToolSelection | undefined, tools: Map, extension: IRelaxedExtensionDescription, logService: ILogService): vscode.ChatRequest { const toolReferences = request.variables.variables.filter(v => v.kind === 'tool'); const variableReferences = request.variables.variables.filter(v => v.kind !== 'tool'); const requestWithAllProps: vscode.ChatRequest = { @@ -2930,7 +2931,9 @@ export namespace ChatAgentRequest { attempt: request.attempt ?? 0, enableCommandDetection: request.enableCommandDetection ?? true, isParticipantDetected: request.isParticipantDetected ?? false, - references: variableReferences.map(v => ChatPromptReference.to(v, diagnostics)), + references: variableReferences + .map(v => ChatPromptReference.to(v, diagnostics, logService)) + .filter(isDefined), toolReferences: toolReferences.map(ChatLanguageModelToolReference.to), location: ChatLocation.to(request.location), acceptedConfirmationData: request.acceptedConfirmationData, @@ -2994,10 +2997,18 @@ export namespace ChatLocation { } export namespace ChatPromptReference { - export function to(variable: IChatRequestVariableEntry, diagnostics: readonly [vscode.Uri, readonly vscode.Diagnostic[]][]): vscode.ChatPromptReference { + export function to(variable: IChatRequestVariableEntry, diagnostics: readonly [vscode.Uri, readonly vscode.Diagnostic[]][], logService: ILogService): vscode.ChatPromptReference | undefined { let value: vscode.ChatPromptReference['value'] = variable.value; if (!value) { - throw new Error('Invalid value reference'); + let varStr: string; + try { + varStr = JSON.stringify(variable); + } catch { + varStr = `kind=${variable.kind}, id=${variable.id}, name=${variable.name}`; + } + + logService.error(`[ChatPromptReference] Ignoring invalid reference in variable: ${varStr}`); + return undefined; } if (isUriComponents(value)) { diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 37e3fa81713..4b839c1dc8d 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -4724,6 +4724,16 @@ export class ChatResponseNotebookEditPart implements vscode.ChatResponseNotebook } } +export class ChatPrepareToolInvocationPart { + toolName: string; + /** + * @param toolName The name of the tool being prepared for invocation. + */ + constructor(toolName: string) { + this.toolName = toolName; + } +} + export class ChatRequestTurn implements vscode.ChatRequestTurn2 { constructor( readonly prompt: string, diff --git a/src/vs/workbench/api/node/extHostDebugService.ts b/src/vs/workbench/api/node/extHostDebugService.ts index b325f95bb0d..768742ba438 100644 --- a/src/vs/workbench/api/node/extHostDebugService.ts +++ b/src/vs/workbench/api/node/extHostDebugService.ts @@ -31,8 +31,6 @@ import { IExtHostTerminalShellIntegration } from '../common/extHostTerminalShell export class ExtHostDebugService extends ExtHostDebugServiceBase { - override readonly _serviceBrand: undefined; - private _integratedTerminalInstances = new DebugTerminalCollection(); private _terminalDisposedListener: IDisposable | undefined; diff --git a/src/vs/workbench/api/test/browser/extHostMessagerService.test.ts b/src/vs/workbench/api/test/browser/extHostMessagerService.test.ts index bb5ff5e46ff..bedd41eb4db 100644 --- a/src/vs/workbench/api/test/browser/extHostMessagerService.test.ts +++ b/src/vs/workbench/api/test/browser/extHostMessagerService.test.ts @@ -26,8 +26,6 @@ const emptyCommandService: ICommandService = { const emptyNotificationService = new class implements INotificationService { declare readonly _serviceBrand: undefined; - onDidAddNotification: Event = Event.None; - onDidRemoveNotification: Event = Event.None; onDidChangeFilter: Event = Event.None; notify(...args: any[]): never { throw new Error('not implemented'); @@ -67,8 +65,6 @@ class EmptyNotificationService implements INotificationService { constructor(private withNotify: (notification: INotification) => void) { } - onDidAddNotification: Event = Event.None; - onDidRemoveNotification: Event = Event.None; onDidChangeFilter: Event = Event.None; notify(notification: INotification): INotificationHandle { this.withNotify(notification); diff --git a/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts b/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts index d4db3411faf..12b33dd55ce 100644 --- a/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts +++ b/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts @@ -137,7 +137,7 @@ export class BrowserDialogHandler extends AbstractDialogHandler { renderBody, icon: customOptions?.icon, disableCloseAction: customOptions?.disableCloseAction, - buttonDetails: customOptions?.buttonDetails, + buttonOptions: customOptions?.buttonDetails?.map(detail => ({ sublabel: detail })), checkboxLabel: checkbox?.label, checkboxChecked: checkbox?.checked, inputs diff --git a/src/vs/workbench/browser/parts/editor/auxiliaryEditorPart.ts b/src/vs/workbench/browser/parts/editor/auxiliaryEditorPart.ts index 59276b6858f..f127c88878e 100644 --- a/src/vs/workbench/browser/parts/editor/auxiliaryEditorPart.ts +++ b/src/vs/workbench/browser/parts/editor/auxiliaryEditorPart.ts @@ -89,7 +89,6 @@ registerAction2(class extends Action2 { id: 'workbench.action.disableCompactAuxiliaryWindow', title: localize('disableCompactAuxiliaryWindow', "Unset Compact Mode"), icon: Codicon.screenNormal, - toggled: ContextKeyExpr.and(IsAuxiliaryTitleBarContext, IsCompactTitleBarContext), menu: { id: MenuId.LayoutControlMenu, when: ContextKeyExpr.and(IsAuxiliaryTitleBarContext, IsCompactTitleBarContext), @@ -342,7 +341,8 @@ class AuxiliaryEditorPartImpl extends EditorPart implements IAuxiliaryEditorPart updateOptions(options: { compact: boolean }): void { if (options.compact && !this.optionsDisposable.value) { this.optionsDisposable.value = this.enforcePartOptions({ - showTabs: 'none' + showTabs: 'none', + closeEmptyGroups: true }); } else if (!options.compact) { this.optionsDisposable.clear(); diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index f78f3150637..adefa72af23 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -9,7 +9,7 @@ import { GroupIdentifier, CloseDirection, IEditorCloseEvent, IEditorPane, SaveRe import { ActiveEditorGroupLockedContext, ActiveEditorDirtyContext, EditorGroupEditorsCountContext, ActiveEditorStickyContext, ActiveEditorPinnedContext, ActiveEditorLastInGroupContext, ActiveEditorFirstInGroupContext, ResourceContextKey, applyAvailableEditorIds, ActiveEditorAvailableEditorIdsContext, ActiveEditorCanSplitInGroupContext, SideBySideEditorActiveContext, TextCompareEditorVisibleContext, TextCompareEditorActiveContext, ActiveEditorContext, ActiveEditorReadonlyContext, ActiveEditorCanRevertContext, ActiveEditorCanToggleReadonlyContext, ActiveCompareEditorCanSwapContext, MultipleEditorsSelectedInGroupContext, TwoEditorsSelectedInGroupContext, SelectedEditorsInGroupFileOrUntitledResourceContextKey } from '../../../common/contextkeys.js'; import { EditorInput } from '../../../common/editor/editorInput.js'; import { SideBySideEditorInput } from '../../../common/editor/sideBySideEditorInput.js'; -import { Emitter, Relay } from '../../../../base/common/event.js'; +import { Emitter, Event, Relay } from '../../../../base/common/event.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { Dimension, trackFocus, addDisposableListener, EventType, EventHelper, findParentWithClass, isAncestor, IDomNodePagePosition, isMouseEvent, isActiveElement, getWindow, getActiveElement, $ } from '../../../../base/browser/dom.js'; import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js'; @@ -32,7 +32,7 @@ import { IEditorGroupsView, IEditorGroupView, fillActiveEditorViewState, EditorS import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js'; import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; import { SubmenuAction } from '../../../../base/common/actions.js'; -import { IMenuService, MenuId } from '../../../../platform/actions/common/actions.js'; +import { IMenuChangeEvent, IMenuService, MenuId } from '../../../../platform/actions/common/actions.js'; import { StandardMouseEvent } from '../../../../base/browser/mouseEvent.js'; import { getActionBarActions, PrimaryAndSecondaryActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; @@ -2086,8 +2086,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { createEditorActions(disposables: DisposableStore): IActiveEditorActions { let actions: PrimaryAndSecondaryActions = { primary: [], secondary: [] }; - - let onDidChange; + let onDidChange: Event | undefined; // Editor actions require the editor control to be there, so we retrieve it via service const activeEditorPane = this.activeEditorPane; @@ -2106,9 +2105,9 @@ export class EditorGroupView extends Themable implements IEditorGroupView { } else { // If there is no active pane in the group (it's the last group and it's empty) // Trigger the change event when the active editor changes - const _onDidChange = disposables.add(new Emitter()); - onDidChange = _onDidChange.event; - disposables.add(this.onDidActiveEditorChange(() => _onDidChange.fire())); + const onDidChangeEmitter = disposables.add(new Emitter()); + onDidChange = onDidChangeEmitter.event; + disposables.add(this.onDidActiveEditorChange(() => onDidChangeEmitter.fire())); } return { actions, onDidChange }; diff --git a/src/vs/workbench/browser/parts/editor/editorParts.ts b/src/vs/workbench/browser/parts/editor/editorParts.ts index 14daf210d0c..3dca3186361 100644 --- a/src/vs/workbench/browser/parts/editor/editorParts.ts +++ b/src/vs/workbench/browser/parts/editor/editorParts.ts @@ -7,7 +7,7 @@ import { localize } from '../../../../nls.js'; import { EditorGroupLayout, GroupDirection, GroupLocation, GroupOrientation, GroupsArrangement, GroupsOrder, IAuxiliaryEditorPart, IEditorGroupContextKeyProvider, IEditorDropTargetDelegate, IEditorGroupsService, IEditorSideGroup, IEditorWorkingSet, IFindGroupScope, IMergeGroupOptions, IEditorWorkingSetOptions, IEditorPart } from '../../../services/editor/common/editorGroupsService.js'; import { Emitter } from '../../../../base/common/event.js'; import { DisposableMap, DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; -import { GroupIdentifier } from '../../../common/editor.js'; +import { GroupIdentifier, IEditorPartOptions } from '../../../common/editor.js'; import { EditorPart, IEditorPartUIState, MainEditorPart } from './editorPart.js'; import { IEditorGroupView, IEditorPartsView } from './editor.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; @@ -24,6 +24,7 @@ import { ContextKeyValue, IContextKey, IContextKeyService, RawContextKey } from import { isHTMLElement } from '../../../../base/browser/dom.js'; import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; +import { DeepPartial } from '../../../../base/common/types.js'; interface IEditorPartsUIState { readonly auxiliary: IAuxiliaryEditorPartState[]; @@ -57,6 +58,14 @@ export class EditorParts extends MultiWindowParts implements IEditor ) { super('workbench.editorParts', themeService, storageService); + this.editorWorkingSets = (() => { + const workingSetsRaw = this.storageService.get(EditorParts.EDITOR_WORKING_SETS_STORAGE_KEY, StorageScope.WORKSPACE); + if (workingSetsRaw) { + return JSON.parse(workingSetsRaw); + } + return []; + })(); + this.mainPart = this._register(this.createMainEditorPart()); this._register(this.registerPart(this.mainPart)); @@ -366,14 +375,7 @@ export class EditorParts extends MultiWindowParts implements IEditor private static readonly EDITOR_WORKING_SETS_STORAGE_KEY = 'editor.workingSets'; - private editorWorkingSets: IEditorWorkingSetState[] = (() => { - const workingSetsRaw = this.storageService.get(EditorParts.EDITOR_WORKING_SETS_STORAGE_KEY, StorageScope.WORKSPACE); - if (workingSetsRaw) { - return JSON.parse(workingSetsRaw); - } - - return []; - })(); + private editorWorkingSets: IEditorWorkingSetState[]; saveWorkingSet(name: string): IEditorWorkingSet { const workingSet: IEditorWorkingSetState = { @@ -811,6 +813,10 @@ export class EditorParts extends MultiWindowParts implements IEditor get partOptions() { return this.mainPart.partOptions; } get onDidChangeEditorPartOptions() { return this.mainPart.onDidChangeEditorPartOptions; } + enforcePartOptions(options: DeepPartial): IDisposable { + return this.mainPart.enforcePartOptions(options); + } + //#endregion } diff --git a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts index 6c4b7974cad..600015d4a8b 100644 --- a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts @@ -308,7 +308,7 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart { @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, @IContextKeyService contextKeyService: IContextKeyService, @IHostService private readonly hostService: IHostService, - @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, + @IEditorGroupsService editorGroupService: IEditorGroupsService, @IEditorService editorService: IEditorService, @IMenuService private readonly menuService: IMenuService, @IKeybindingService private readonly keybindingService: IKeybindingService @@ -341,7 +341,7 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart { this._register(this.hostService.onDidChangeFocus(focused => focused ? this.onFocus() : this.onBlur())); this._register(this.hostService.onDidChangeActiveWindow(windowId => windowId === targetWindowId ? this.onFocus() : this.onBlur())); this._register(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationChanged(e))); - this._register(this.editorGroupService.onDidChangeEditorPartOptions(e => this.onEditorPartConfigurationChange(e))); + this._register(this.editorGroupsContainer.onDidChangeEditorPartOptions(e => this.onEditorPartConfigurationChange(e))); } private onBlur(): void { @@ -820,11 +820,11 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart { } private get editorActionsEnabled(): boolean { - return !this.isCompact && this.editorGroupService.partOptions.editorActionsLocation === EditorActionsLocation.TITLEBAR || + return !this.isCompact && (this.editorGroupsContainer.partOptions.editorActionsLocation === EditorActionsLocation.TITLEBAR || ( - this.editorGroupService.partOptions.editorActionsLocation === EditorActionsLocation.DEFAULT && - this.editorGroupService.partOptions.showTabs === EditorTabsMode.NONE - ); + this.editorGroupsContainer.partOptions.editorActionsLocation === EditorActionsLocation.DEFAULT && + this.editorGroupsContainer.partOptions.showTabs === EditorTabsMode.NONE + )); } private get activityActionsEnabled(): boolean { diff --git a/src/vs/workbench/contrib/accessibilitySignals/browser/editorTextPropertySignalsContribution.ts b/src/vs/workbench/contrib/accessibilitySignals/browser/editorTextPropertySignalsContribution.ts index 28d6fe3edef..f81c2f5f4e1 100644 --- a/src/vs/workbench/contrib/accessibilitySignals/browser/editorTextPropertySignalsContribution.ts +++ b/src/vs/workbench/contrib/accessibilitySignals/browser/editorTextPropertySignalsContribution.ts @@ -20,34 +20,11 @@ import { IEditorService } from '../../../services/editor/common/editorService.js import { IDebugService } from '../../debug/common/debug.js'; export class EditorTextPropertySignalsContribution extends Disposable implements IWorkbenchContribution { - private readonly _textProperties: TextProperty[] = [ - this._instantiationService.createInstance(MarkerTextProperty, AccessibilitySignal.errorAtPosition, AccessibilitySignal.errorOnLine, MarkerSeverity.Error), - this._instantiationService.createInstance(MarkerTextProperty, AccessibilitySignal.warningAtPosition, AccessibilitySignal.warningOnLine, MarkerSeverity.Warning), - this._instantiationService.createInstance(FoldedAreaTextProperty), - this._instantiationService.createInstance(BreakpointTextProperty), - ]; + private readonly _textProperties: TextProperty[]; - private readonly _someAccessibilitySignalIsEnabled = derived(this, reader => - this._textProperties - .flatMap(p => [p.lineSignal, p.positionSignal]) - .filter(isDefined) - .some(signal => observableFromValueWithChangeEvent(this, this._accessibilitySignalService.getEnabledState(signal, false)).read(reader)) - ); + private readonly _someAccessibilitySignalIsEnabled; - private readonly _activeEditorObservable = observableFromEvent(this, - this._editorService.onDidActiveEditorChange, - (_) => { - const activeTextEditorControl = this._editorService.activeTextEditorControl; - - const editor = isDiffEditor(activeTextEditorControl) - ? activeTextEditorControl.getOriginalEditor() - : isCodeEditor(activeTextEditorControl) - ? activeTextEditorControl - : undefined; - - return editor && editor.hasModel() ? { editor, model: editor.getModel() } : undefined; - } - ); + private readonly _activeEditorObservable; constructor( @IEditorService private readonly _editorService: IEditorService, @@ -55,6 +32,32 @@ export class EditorTextPropertySignalsContribution extends Disposable implements @IAccessibilitySignalService private readonly _accessibilitySignalService: IAccessibilitySignalService ) { super(); + this._textProperties = [ + this._instantiationService.createInstance(MarkerTextProperty, AccessibilitySignal.errorAtPosition, AccessibilitySignal.errorOnLine, MarkerSeverity.Error), + this._instantiationService.createInstance(MarkerTextProperty, AccessibilitySignal.warningAtPosition, AccessibilitySignal.warningOnLine, MarkerSeverity.Warning), + this._instantiationService.createInstance(FoldedAreaTextProperty), + this._instantiationService.createInstance(BreakpointTextProperty), + ]; + this._someAccessibilitySignalIsEnabled = derived(this, reader => + this._textProperties + .flatMap(p => [p.lineSignal, p.positionSignal]) + .filter(isDefined) + .some(signal => observableFromValueWithChangeEvent(this, this._accessibilitySignalService.getEnabledState(signal, false)).read(reader)) + ); + this._activeEditorObservable = observableFromEvent(this, + this._editorService.onDidActiveEditorChange, + (_) => { + const activeTextEditorControl = this._editorService.activeTextEditorControl; + + const editor = isDiffEditor(activeTextEditorControl) + ? activeTextEditorControl.getOriginalEditor() + : isCodeEditor(activeTextEditorControl) + ? activeTextEditorControl + : undefined; + + return editor && editor.hasModel() ? { editor, model: editor.getModel() } : undefined; + } + ); this._register(autorunWithStore((reader, store) => { /** @description updateSignalsEnabled */ diff --git a/src/vs/workbench/contrib/authentication/browser/authentication.contribution.ts b/src/vs/workbench/contrib/authentication/browser/authentication.contribution.ts index 59a151ce08c..1694e0ccbfd 100644 --- a/src/vs/workbench/contrib/authentication/browser/authentication.contribution.ts +++ b/src/vs/workbench/contrib/authentication/browser/authentication.contribution.ts @@ -42,6 +42,7 @@ class AuthenticationDataRenderer extends Disposable implements IExtensionFeature const headers = [ localize('authenticationlabel', "Label"), localize('authenticationid', "ID"), + localize('authenticationMcpAuthorizationServers', "MCP Authorization Servers") ]; const rows: IRowData[][] = authentication @@ -50,6 +51,7 @@ class AuthenticationDataRenderer extends Disposable implements IExtensionFeature return [ auth.label, auth.id, + (auth.issuerGlobs ?? []).join(',\n') ]; }); diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index 9915576ea87..6696185edba 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -49,7 +49,7 @@ import { EXTENSIONS_CATEGORY, IExtensionsWorkbenchService } from '../../../exten import { IChatAgentService } from '../../common/chatAgents.js'; import { ChatContextKeys } from '../../common/chatContextKeys.js'; import { IChatEditingSession, ModifiedFileEntryState } from '../../common/chatEditingService.js'; -import { ChatEntitlement, ChatSentiment, IChatEntitlementService } from '../../common/chatEntitlementService.js'; +import { ChatEntitlement, IChatEntitlementService } from '../../common/chatEntitlementService.js'; import { extractAgentAndCommand } from '../../common/chatParserTypes.js'; import { IChatDetail, IChatService } from '../../common/chatService.js'; import { IChatRequestViewModel, IChatResponseViewModel, isRequestVM } from '../../common/chatViewModel.js'; @@ -71,6 +71,7 @@ export const ACTION_ID_NEW_EDIT_SESSION = `workbench.action.chat.newEditSession` export const CHAT_OPEN_ACTION_ID = 'workbench.action.chat.open'; export const CHAT_SETUP_ACTION_ID = 'workbench.action.chat.triggerSetup'; const TOGGLE_CHAT_ACTION_ID = 'workbench.action.chat.toggle'; +const CHAT_CLEAR_HISTORY_ACTION_ID = 'workbench.action.chat.clearHistory'; export interface IChatViewOpenOptions { /** @@ -113,7 +114,10 @@ abstract class OpenChatGlobalAction extends Action2 { icon: Codicon.copilot, f1: true, category: CHAT_CATEGORY, - precondition: ChatContextKeys.Setup.hidden.negate(), + precondition: ContextKeyExpr.and( + ChatContextKeys.Setup.hidden.negate(), + ChatContextKeys.Setup.disabled.negate() + ) }); } @@ -346,6 +350,7 @@ export function registerChatActions() { const viewsService = accessor.get(IViewsService); const editorService = accessor.get(IEditorService); const dialogService = accessor.get(IDialogService); + const commandService = accessor.get(ICommandService); const view = await viewsService.openView(ChatViewId); if (!view) { @@ -366,6 +371,11 @@ export function registerChatActions() { } const showPicker = async () => { + const clearChatHistoryButton: IQuickInputButton = { + iconClass: ThemeIcon.asClassName(Codicon.clearAll), + tooltip: localize('interactiveSession.history.clear', "Clear All Workspace Chats"), + }; + const openInEditorButton: IQuickInputButton = { iconClass: ThemeIcon.asClassName(Codicon.file), tooltip: localize('interactiveSession.history.editor', "Open in Editor"), @@ -414,9 +424,16 @@ export function registerChatActions() { const store = new DisposableStore(); const picker = store.add(quickInputService.createQuickPick({ useSeparators: true })); + picker.title = localize('interactiveSession.history.title', "Workspace Chat History"); picker.placeholder = localize('interactiveSession.history.pick', "Switch to chat"); + picker.buttons = [clearChatHistoryButton]; const picks = await getPicks(); picker.items = picks; + store.add(picker.onDidTriggerButton(async button => { + if (button === clearChatHistoryButton) { + await commandService.executeCommand(CHAT_CLEAR_HISTORY_ACTION_ID); + } + })); store.add(picker.onDidTriggerItemButton(async context => { if (context.button === openInEditorButton) { const options: IChatEditorOptions = { target: { sessionId: context.item.chat.sessionId }, pinned: true }; @@ -479,7 +496,7 @@ export function registerChatActions() { f1: false, category: CHAT_CATEGORY, menu: { - id: MenuId.ChatInput, + id: MenuId.ChatExecute, when: ChatContextKeys.chatMode.isEqualTo(ChatMode.Ask), group: 'navigation', order: 1 @@ -533,7 +550,7 @@ export function registerChatActions() { registerAction2(class ClearChatHistoryAction extends Action2 { constructor() { super({ - id: 'workbench.action.chat.clearHistory', + id: CHAT_CLEAR_HISTORY_ACTION_ID, title: localize2('chat.clear.label', "Clear All Workspace Chats"), precondition: ChatContextKeys.enabled, category: CHAT_CATEGORY, @@ -683,7 +700,10 @@ export function registerChatActions() { super({ id: 'workbench.action.chat.configureCodeCompletions', title: localize2('configureCompletions', "Configure Code Completions..."), - precondition: ChatContextKeys.Setup.installed, + precondition: ContextKeyExpr.and( + ChatContextKeys.Setup.installed, + ChatContextKeys.Setup.disabled.negate() + ), menu: { id: MenuId.ChatTitleBarMenu, group: 'f_completions', @@ -760,6 +780,22 @@ export function registerChatActions() { }); } }); + + registerAction2(class ResetTrustedToolsAction extends Action2 { + constructor() { + super({ + id: 'workbench.action.chat.resetTrustedTools', + title: localize2('resetTrustedTools', "Reset Tool Confirmations"), + category: CHAT_CATEGORY, + f1: true, + precondition: ChatContextKeys.enabled + }); + } + override run(accessor: ServicesAccessor): void { + accessor.get(ILanguageModelToolsService).resetToolAutoConfirmation(); + accessor.get(INotificationService).info(localize('resetTrustedToolsSuccess', "Tool confirmation preferences have been reset.")); + } + }); } export function stringifyItem(item: IChatRequestViewModel | IChatResponseViewModel, includeName = true): string { @@ -834,21 +870,6 @@ registerAction2(class ToggleCopilotControl extends ToggleTitleBarConfigAction { } }); -registerAction2(class ResetTrustedToolsAction extends Action2 { - constructor() { - super({ - id: 'workbench.action.chat.resetTrustedTools', - title: localize2('resetTrustedTools', "Reset Tool Confirmations"), - category: CHAT_CATEGORY, - f1: true, - }); - } - override run(accessor: ServicesAccessor): void { - accessor.get(ILanguageModelToolsService).resetToolAutoConfirmation(); - accessor.get(INotificationService).info(localize('resetTrustedToolsSuccess', "Tool confirmation preferences have been reset.")); - } -}); - export class CopilotTitleBarMenuRendering extends Disposable implements IWorkbenchContribution { static readonly ID = 'workbench.contrib.copilotTitleBarMenuRendering'; @@ -871,7 +892,7 @@ export class CopilotTitleBarMenuRendering extends Disposable implements IWorkben run() { } }); - const chatExtensionInstalled = chatEntitlementService.sentiment === ChatSentiment.Installed; + const chatSentiment = chatEntitlementService.sentiment; const chatQuotaExceeded = chatEntitlementService.quotas.chat?.percentRemaining === 0; const signedOut = chatEntitlementService.entitlement === ChatEntitlement.Unknown; const limited = chatEntitlementService.entitlement === ChatEntitlement.Limited; @@ -879,7 +900,7 @@ export class CopilotTitleBarMenuRendering extends Disposable implements IWorkben let primaryActionId = TOGGLE_CHAT_ACTION_ID; let primaryActionTitle = localize('toggleChat', "Toggle Chat"); let primaryActionIcon = Codicon.copilot; - if (chatExtensionInstalled) { + if (chatSentiment.installed && !chatSentiment.disabled) { if (signedOut) { primaryActionId = CHAT_SETUP_ACTION_ID; primaryActionTitle = localize('signInToChatSetup', "Sign in to use Copilot..."); diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts index 85ec8d49d88..45884821c9c 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts @@ -13,7 +13,7 @@ import { CommandsRegistry } from '../../../../../platform/commands/common/comman import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; -import { ActiveEditorContext } from '../../../../common/contextkeys.js'; +import { ActiveEditorContext, IsAuxiliaryTitleBarContext, IsCompactTitleBarContext } from '../../../../common/contextkeys.js'; import { ChatContextKeys } from '../../common/chatContextKeys.js'; import { IChatEditingSession } from '../../common/chatEditingService.js'; import { ChatMode } from '../../common/constants.js'; @@ -55,6 +55,15 @@ export function registerNewChatActions() { group: 'navigation', order: 0, when: ActiveEditorContext.isEqualTo(ChatEditorInput.EditorID), + }, + { + id: MenuId.LayoutControlMenu, + when: ContextKeyExpr.and( + ActiveEditorContext.isEqualTo(ChatEditorInput.EditorID), + IsAuxiliaryTitleBarContext, + IsCompactTitleBarContext + ), + order: -1 }] }); } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContext.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContext.ts new file mode 100644 index 00000000000..60a961324b2 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContext.ts @@ -0,0 +1,289 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { CancellationToken } from '../../../../../base/common/cancellation.js'; +import { Codicon } from '../../../../../base/common/codicons.js'; +import { Disposable } from '../../../../../base/common/lifecycle.js'; +import { isElectron } from '../../../../../base/common/platform.js'; +import { dirname } from '../../../../../base/common/resources.js'; +import { ThemeIcon } from '../../../../../base/common/themables.js'; +import { localize } from '../../../../../nls.js'; +import { IClipboardService } from '../../../../../platform/clipboard/common/clipboardService.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { ILabelService } from '../../../../../platform/label/common/label.js'; +import { IQuickPickSeparator } from '../../../../../platform/quickinput/common/quickInput.js'; +import { IWorkbenchContribution } from '../../../../common/contributions.js'; +import { EditorResourceAccessor, SideBySideEditor } from '../../../../common/editor.js'; +import { DiffEditorInput } from '../../../../common/editor/diffEditorInput.js'; +import { IEditorService } from '../../../../services/editor/common/editorService.js'; +import { IHostService } from '../../../../services/host/browser/host.js'; +import { UntitledTextEditorInput } from '../../../../services/untitled/common/untitledTextEditorInput.js'; +import { FileEditorInput } from '../../../files/browser/editors/fileEditorInput.js'; +import { NotebookEditorInput } from '../../../notebook/common/notebookEditorInput.js'; +import { IChatContextPickService, IChatContextValueItem, IChatContextPickerItem, IChatContextPickerPickItem } from '../chatContextPickService.js'; +import { IChatEditingService } from '../../common/chatEditingService.js'; +import { IChatRequestToolEntry, IChatRequestVariableEntry, IImageVariableEntry, OmittedState } from '../../common/chatModel.js'; +import { IToolData } from '../../common/languageModelToolsService.js'; +import { IChatWidget } from '../chat.js'; +import { imageToHash, isImage } from '../chatPasteProviders.js'; +import { convertBufferToScreenshotVariable } from '../contrib/screenshot.js'; +import { ChatInstructionsPickerPick } from './promptActions/chatAttachInstructionsAction.js'; + + +export class ChatContextContributions extends Disposable implements IWorkbenchContribution { + + static readonly ID = 'chat.contextContributions'; + + constructor( + @IInstantiationService instantiationService: IInstantiationService, + @IChatContextPickService contextPickService: IChatContextPickService, + ) { + super(); + + // ############################################################################################### + // + // Default context picks/values which are "native" to chat. This is NOT the complete list + // and feature area specific context, like for notebooks, problems, etc, should be contributed + // by the feature area. + // + // ############################################################################################### + + this._store.add(contextPickService.registerChatContextItem(instantiationService.createInstance(ToolsContextPickerPick))); + this._store.add(contextPickService.registerChatContextItem(instantiationService.createInstance(ChatInstructionsPickerPick))); + this._store.add(contextPickService.registerChatContextItem(instantiationService.createInstance(OpenEditorContextValuePick))); + this._store.add(contextPickService.registerChatContextItem(instantiationService.createInstance(RelatedFilesContextPickerPick))); + this._store.add(contextPickService.registerChatContextItem(instantiationService.createInstance(ClipboardImageContextValuePick))); + this._store.add(contextPickService.registerChatContextItem(instantiationService.createInstance(ScreenshotContextValuePick))); + } +} + +class ToolsContextPickerPick implements IChatContextPickerItem { + + readonly type = 'pickerPick'; + readonly label: string = localize('chatContext.tools', 'Tools...'); + readonly icon: ThemeIcon = Codicon.tools; + readonly ordinal = -500; + + asPicker(widget: IChatWidget): { + readonly placeholder: string; + readonly picks: Promise<(IChatContextPickerPickItem | IQuickPickSeparator)[]>; + } { + + type Pick = IChatContextPickerPickItem & { ordinal: number; groupLabel: string }; + const items: Pick[] = []; + + for (const tool of widget.input.selectedToolsModel.tools.get()) { + if (!tool.canBeReferencedInPrompt) { + continue; + } + const item: Pick = { + ...this._classify(tool), + label: tool.toolReferenceName ?? tool.id, + description: (tool.toolReferenceName ?? tool.id) !== tool.displayName ? tool.displayName : undefined, + asAttachment: (): IChatRequestToolEntry => { + return { + kind: 'tool', + id: tool.id, + name: tool.displayName, + fullName: tool.displayName, + value: undefined, + }; + } + }; + + items.push(item); + } + + items.sort((a, b) => { + let res = a.ordinal - b.ordinal; + if (res === 0) { + res = a.label.localeCompare(b.label); + } + return res; + }); + + let lastGroupLabel: string | undefined; + const picks: (IQuickPickSeparator | Pick)[] = []; + + + for (const item of items) { + if (lastGroupLabel !== item.groupLabel) { + picks.push({ type: 'separator', label: item.groupLabel }); + lastGroupLabel = item.groupLabel; + } + picks.push(item); + } + + return { + placeholder: localize('chatContext.tools.placeholder', 'Select a tool'), + picks: Promise.resolve(picks) + }; + + } + + private _classify(tool: IToolData) { + if (tool.source.type === 'internal' || tool.source.type === 'extension' && !tool.source.isExternalTool) { + return { ordinal: 1, groupLabel: localize('chatContext.tools.internal', 'Built-In') }; + } else if (tool.source.type === 'mcp') { + return { ordinal: 2, groupLabel: localize('chatContext.tools.mcp', 'MCP Servers') }; + } else { + return { ordinal: 3, groupLabel: localize('chatContext.tools.extension', 'Extensions') }; + } + } +} + + + +class OpenEditorContextValuePick implements IChatContextValueItem { + + readonly type = 'valuePick'; + readonly label: string = localize('chatContext.editors', 'Open Editors'); + readonly icon: ThemeIcon = Codicon.file; + readonly ordinal = 800; + + constructor( + @IEditorService private _editorService: IEditorService, + @ILabelService private _labelService: ILabelService, + ) { } + + isEnabled(): Promise | boolean { + return this._editorService.editors.filter(e => e instanceof FileEditorInput || e instanceof DiffEditorInput || e instanceof UntitledTextEditorInput).length > 0; + } + + async asAttachment(): Promise { + const result: IChatRequestVariableEntry[] = []; + for (const editor of this._editorService.editors) { + if (!(editor instanceof FileEditorInput || editor instanceof DiffEditorInput || editor instanceof UntitledTextEditorInput || editor instanceof NotebookEditorInput)) { + continue; + } + const uri = EditorResourceAccessor.getOriginalUri(editor, { supportSideBySide: SideBySideEditor.PRIMARY }); + if (!uri) { + continue; + } + result.push({ + kind: 'file', + id: uri.toString(), + value: uri, + name: this._labelService.getUriBasenameLabel(uri), + }); + } + return result; + } + +} + +class RelatedFilesContextPickerPick implements IChatContextPickerItem { + + readonly type = 'pickerPick'; + + readonly label: string = localize('chatContext.relatedFiles', 'Related Files'); + readonly icon: ThemeIcon = Codicon.sparkle; + readonly ordinal = 300; + + constructor( + @IChatEditingService private readonly _chatEditingService: IChatEditingService, + @ILabelService private readonly _labelService: ILabelService, + ) { } + + isEnabled(widget: IChatWidget): boolean { + return this._chatEditingService.hasRelatedFilesProviders() && (Boolean(widget.getInput()) || widget.attachmentModel.fileAttachments.length > 0); + } + + asPicker(widget: IChatWidget): { + readonly placeholder: string; + readonly picks: Promise<(IChatContextPickerPickItem | IQuickPickSeparator)[]>; + } { + + const picks = (async () => { + const chatSessionId = widget.viewModel?.sessionId; + if (!chatSessionId) { + return []; + } + const relatedFiles = await this._chatEditingService.getRelatedFiles(chatSessionId, widget.getInput(), widget.attachmentModel.fileAttachments, CancellationToken.None); + if (!relatedFiles) { + return []; + } + const attachments = widget.attachmentModel.getAttachmentIDs(); + return this._chatEditingService.getRelatedFiles(chatSessionId, widget.getInput(), widget.attachmentModel.fileAttachments, CancellationToken.None) + .then((files) => (files ?? []).reduce<(IChatContextPickerPickItem | IQuickPickSeparator)[]>((acc, cur) => { + acc.push({ type: 'separator', label: cur.group }); + for (const file of cur.files) { + const label = this._labelService.getUriBasenameLabel(file.uri); + acc.push({ + label: label, + description: this._labelService.getUriLabel(dirname(file.uri), { relative: true }), + disabled: attachments.has(file.uri.toString()), + asAttachment: () => { + return { + kind: 'file', + id: file.uri.toString(), + value: file.uri, + name: label, + omittedState: OmittedState.NotOmitted + }; + } + }); + } + return acc; + }, [])); + })(); + + return { + placeholder: localize('relatedFiles', 'Add related files to your working set'), + picks, + }; + } +} + + +class ClipboardImageContextValuePick implements IChatContextValueItem { + readonly type = 'valuePick'; + readonly label = localize('imageFromClipboard', 'Image from Clipboard'); + readonly icon = Codicon.fileMedia; + + constructor( + @IClipboardService private readonly _clipboardService: IClipboardService, + ) { } + + async isEnabled(widget: IChatWidget) { + if (!widget.input.selectedLanguageModel?.metadata.capabilities?.vision) { + return false; + } + const imageData = await this._clipboardService.readImage(); + return isImage(imageData); + } + + async asAttachment(): Promise { + const fileBuffer = await this._clipboardService.readImage(); + return { + id: await imageToHash(fileBuffer), + name: localize('pastedImage', 'Pasted Image'), + fullName: localize('pastedImage', 'Pasted Image'), + value: fileBuffer, + kind: 'image', + }; + } +} + +class ScreenshotContextValuePick implements IChatContextValueItem { + + readonly type = 'valuePick'; + readonly icon = Codicon.deviceCamera; + readonly label = (isElectron + ? localize('chatContext.attachScreenshot.labelElectron.Window', 'Screenshot Window') + : localize('chatContext.attachScreenshot.labelWeb', 'Screenshot')); + + constructor( + @IHostService private readonly _hostService: IHostService, + ) { } + + async isEnabled(widget: IChatWidget) { + return !!widget.input.selectedLanguageModel?.metadata.capabilities?.vision; + } + + async asAttachment(): Promise { + const blob = await this._hostService.getScreenshot(); + return blob && convertBufferToScreenshotVariable(blob); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts index dfb0dbb7638..ca37ab55811 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts @@ -3,69 +3,47 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { groupBy } from '../../../../../base/common/arrays.js'; -import { CancellationToken } from '../../../../../base/common/cancellation.js'; +import { DeferredPromise, isThenable, raceCancellationError } from '../../../../../base/common/async.js'; +import { CancellationTokenSource } from '../../../../../base/common/cancellation.js'; import { Codicon } from '../../../../../base/common/codicons.js'; -import { ResolvedKeybinding } from '../../../../../base/common/keybindings.js'; import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; import { DisposableStore } from '../../../../../base/common/lifecycle.js'; import { Schemas } from '../../../../../base/common/network.js'; -import { isElectron } from '../../../../../base/common/platform.js'; -import { basename, dirname, extUri } from '../../../../../base/common/resources.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; -import { WithUriValue } from '../../../../../base/common/types.js'; +import { assertType, isObject } from '../../../../../base/common/types.js'; import { URI } from '../../../../../base/common/uri.js'; import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; -import { IRange, Range } from '../../../../../editor/common/core/range.js'; -import { Command, SymbolKinds } from '../../../../../editor/common/languages.js'; +import { Range } from '../../../../../editor/common/core/range.js'; import { ITextModelService } from '../../../../../editor/common/services/resolverService.js'; import { AbstractGotoSymbolQuickAccessProvider, IGotoSymbolQuickPickItem } from '../../../../../editor/contrib/quickAccess/browser/gotoSymbolQuickAccess.js'; import { localize, localize2 } from '../../../../../nls.js'; import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js'; -import { IClipboardService } from '../../../../../platform/clipboard/common/clipboardService.js'; -import { ICommandService } from '../../../../../platform/commands/common/commands.js'; import { ContextKeyExpr, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js'; import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; -import { ILabelService } from '../../../../../platform/label/common/label.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; -import { IMarkerService, MarkerSeverity } from '../../../../../platform/markers/common/markers.js'; import { AnythingQuickAccessProviderRunOptions } from '../../../../../platform/quickinput/common/quickAccess.js'; -import { IQuickInputService, IQuickPickItem, IQuickPickItemWithResource, IQuickPickSeparator, QuickPickItem } from '../../../../../platform/quickinput/common/quickInput.js'; +import { IQuickInputService, IQuickPickItem, IQuickPickItemWithResource, QuickPickItem } from '../../../../../platform/quickinput/common/quickInput.js'; import { ActiveEditorContext, TextCompareEditorActiveContext } from '../../../../common/contextkeys.js'; import { EditorResourceAccessor, SideBySideEditor } from '../../../../common/editor.js'; -import { DiffEditorInput } from '../../../../common/editor/diffEditorInput.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; -import { IHostService } from '../../../../services/host/browser/host.js'; -import { VIEW_ID as SEARCH_VIEW_ID } from '../../../../services/search/common/search.js'; -import { UntitledTextEditorInput } from '../../../../services/untitled/common/untitledTextEditorInput.js'; import { IViewsService } from '../../../../services/views/common/viewsService.js'; -import { FileEditorInput } from '../../../files/browser/editors/fileEditorInput.js'; import { TEXT_FILE_EDITOR_ID } from '../../../files/common/files.js'; -import { NotebookEditorInput } from '../../../notebook/common/notebookEditorInput.js'; import { AnythingQuickAccessProvider } from '../../../search/browser/anythingQuickAccess.js'; import { isSearchTreeFileMatch, isSearchTreeMatch } from '../../../search/browser/searchTreeModel/searchTreeCommon.js'; -import { SearchView } from '../../../search/browser/searchView.js'; import { ISymbolQuickPickItem, SymbolsQuickAccessProvider } from '../../../search/browser/symbolsQuickAccess.js'; import { SearchContext } from '../../../search/common/constants.js'; -import { IChatAgentService } from '../../common/chatAgents.js'; import { ChatContextKeys } from '../../common/chatContextKeys.js'; -import { IChatEditingService } from '../../common/chatEditingService.js'; -import { IChatRequestVariableEntry, IDiagnosticVariableEntryFilterData, OmittedState } from '../../common/chatModel.js'; -import { ChatRequestAgentPart } from '../../common/chatParserTypes.js'; +import { IChatRequestVariableEntry, OmittedState } from '../../common/chatModel.js'; import { ChatAgentLocation } from '../../common/constants.js'; -import { IToolData } from '../../common/languageModelToolsService.js'; import { IChatWidget, IChatWidgetService, IQuickChatService, showChatView } from '../chat.js'; -import { imageToHash, isImage } from '../chatPasteProviders.js'; import { isQuickChat } from '../chatWidget.js'; -import { createFilesAndFolderQuickPick } from '../contrib/chatDynamicVariables.js'; -import { convertBufferToScreenshotVariable, ScreenshotVariableId } from '../contrib/screenshot.js'; import { resizeImage } from '../imageUtils.js'; -import { INSTRUCTIONS_COMMAND_ID } from '../promptSyntax/contributions/attachInstructionsCommand.js'; import { CHAT_CATEGORY } from './chatActions.js'; -import { runAttachInstructionsAction, registerPromptActions } from './promptActions/index.js'; +import { IChatContextValueItem, IChatContextPickService, IChatContextPickerItem, isChatContextPickerPickItem } from '../chatContextPickService.js'; +import { registerPromptActions } from './promptActions/index.js'; export function registerChatContextActions() { registerAction2(AttachContextAction); @@ -73,170 +51,7 @@ export function registerChatContextActions() { registerAction2(AttachFolderToChatAction); registerAction2(AttachSelectionToChatAction); registerAction2(AttachSearchResultAction); -} - -/** - * We fill the quickpick with these types, and enable some quick access providers - */ -type IAttachmentQuickPickItem = ICommandVariableQuickPickItem | IWorkspaceSymbolsQuickPickItem - | IToolsQuickPickItem | IToolQuickPickItem - | IImageQuickPickItem | IOpenEditorsQuickPickItem | ISearchResultsQuickPickItem - | IScreenShotQuickPickItem | IRelatedFilesQuickPickItem | IInstructionsQuickPickItem - | IFolderQuickPickItem | IFolderResultQuickPickItem - | IDiagnosticsQuickPickItem | IDiagnosticsQuickPickItemWithFilter; - -function isIAttachmentQuickPickItem(obj: unknown): obj is IAttachmentQuickPickItem { - return ( - typeof obj === 'object' - && obj !== null - && typeof (obj).kind === 'string' - ); -} - -const attachmentsOrdinals: (IAttachmentQuickPickItem['kind'])[] = [ - // bottom-most - 'tools', - 'command', - 'screenshot', - 'image', - 'workspaceSymbol', - 'diagnostic', - 'instructions', - 'related-files', - 'folder', - 'open-editors', - // top-most -]; - -/** - * These are the types that we can get out of the quick pick - */ -type IChatContextQuickPickItem = IAttachmentQuickPickItem | IGotoSymbolQuickPickItem | ISymbolQuickPickItem | IQuickPickItemWithResource; - -function isIGotoSymbolQuickPickItem(obj: unknown): obj is IGotoSymbolQuickPickItem { - return ( - typeof obj === 'object' - && typeof (obj as IGotoSymbolQuickPickItem).symbolName === 'string' - && !!(obj as IGotoSymbolQuickPickItem).uri - && !!(obj as IGotoSymbolQuickPickItem).range); -} - -function isISymbolQuickPickItem(obj: unknown): obj is ISymbolQuickPickItem { - return ( - typeof obj === 'object' - && typeof (obj as ISymbolQuickPickItem).symbol === 'object' - && !!(obj as ISymbolQuickPickItem).symbol); -} - -function isIQuickPickItemWithResource(obj: unknown): obj is IQuickPickItemWithResource { - return ( - typeof obj === 'object' - && typeof (obj as IQuickPickItemWithResource).resource === 'object' - && URI.isUri((obj as IQuickPickItemWithResource).resource)); -} - - -interface IToolsQuickPickItem extends IQuickPickItem { - kind: 'tools'; - id: string; - label: string; -} - -interface IRelatedFilesQuickPickItem extends IQuickPickItem { - kind: 'related-files'; - id: string; - label: string; -} - -interface IFolderQuickPickItem extends IQuickPickItem { - kind: 'folder'; - id: string; - label: string; -} - -interface IFolderResultQuickPickItem extends IQuickPickItem { - kind: 'folder-search-result'; - id: string; - resource: URI; -} - -interface IImageQuickPickItem extends IQuickPickItem { - kind: 'image'; - id: string; -} - -interface ICommandVariableQuickPickItem extends IQuickPickItem { - kind: 'command'; - id: string; - command: Command; - name?: string; - value: unknown; - icon?: ThemeIcon; -} - -interface IToolQuickPickItem extends IQuickPickItem { - kind: 'tool'; - id: string; - name?: string; - icon?: ThemeIcon; - tool: IToolData; -} - -interface IWorkspaceSymbolsQuickPickItem extends IQuickPickItem { - kind: 'workspaceSymbol'; - id: string; -} - -interface IOpenEditorsQuickPickItem extends IQuickPickItem { - kind: 'open-editors'; - id: 'open-editors'; - icon?: ThemeIcon; -} - -interface ISearchResultsQuickPickItem extends IQuickPickItem { - kind: 'search-results'; - id: string; - icon?: ThemeIcon; -} - -interface IScreenShotQuickPickItem extends IQuickPickItem { - kind: 'screenshot'; - id: string; - icon?: ThemeIcon; -} - -interface IDiagnosticsQuickPickItem extends IQuickPickItem { - kind: 'diagnostic'; - id: string; - icon?: ThemeIcon; -} - -interface IDiagnosticsQuickPickItemWithFilter extends IQuickPickItem { - kind: 'diagnostic-filter'; - id: string; - filter: IDiagnosticVariableEntryFilterData; - icon?: ThemeIcon; -} - -/** - * Quick pick item for instructions attachment. - */ -const INSTRUCTION_PICK_ID = 'instructions'; -interface IInstructionsQuickPickItem extends IQuickPickItem { - /** - * The ID of the quick pick item. - */ - id: typeof INSTRUCTION_PICK_ID; - - /** - * Unique kind identifier of the instructions attachment. - */ - kind: typeof INSTRUCTION_PICK_ID; - - /** - * Keybinding of the command. - */ - keybinding?: ResolvedKeybinding; + registerPromptActions(); } abstract class AttachResourceAction extends Action2 { @@ -444,6 +259,38 @@ export class AttachSearchResultAction extends Action2 { } } +/** This is our type */ +interface IContextPickItemItem extends IQuickPickItem { + kind: 'contextPick'; + item: IChatContextValueItem | IChatContextPickerItem; +} + +/** These are the types we get from "platform QP" */ +type IQuickPickServicePickItem = IGotoSymbolQuickPickItem | ISymbolQuickPickItem | IQuickPickItemWithResource; + +function isIContextPickItemItem(obj: unknown): obj is IContextPickItemItem { + return ( + isObject(obj) + && typeof (obj).kind === 'string' + && (obj).kind === 'contextPick' + ); +} + +function isIGotoSymbolQuickPickItem(obj: unknown): obj is IGotoSymbolQuickPickItem { + return ( + isObject(obj) + && typeof (obj as IGotoSymbolQuickPickItem).symbolName === 'string' + && !!(obj as IGotoSymbolQuickPickItem).uri + && !!(obj as IGotoSymbolQuickPickItem).range); +} + +function isIQuickPickItemWithResource(obj: unknown): obj is IQuickPickItemWithResource { + return ( + isObject(obj) + && URI.isUri((obj as IQuickPickItemWithResource).resource)); +} + + export class AttachContextAction extends Action2 { constructor() { @@ -466,216 +313,13 @@ export class AttachContextAction extends Action2 { }); } - private _getFileContextId(item: { resource: URI } | { uri: URI; range: IRange }) { - if ('resource' in item) { - return item.resource.toString(); - } - - return item.uri.toString() + (item.range.startLineNumber !== item.range.endLineNumber ? - `:${item.range.startLineNumber}-${item.range.endLineNumber}` : - `:${item.range.startLineNumber}`); - } - - private async _attachContext(accessor: ServicesAccessor, widget: IChatWidget, isInBackground?: boolean, ...picks: IChatContextQuickPickItem[]) { - const commandService = accessor.get(ICommandService); - const clipboardService = accessor.get(IClipboardService); - const editorService = accessor.get(IEditorService); - const labelService = accessor.get(ILabelService); - const viewsService = accessor.get(IViewsService); - const chatEditingService = accessor.get(IChatEditingService); - const hostService = accessor.get(IHostService); - const fileService = accessor.get(IFileService); - const textModelService = accessor.get(ITextModelService); - const quickInputService = accessor.get(IQuickInputService); - const toAttach: IChatRequestVariableEntry[] = []; - for (const pick of picks) { - - if (isIAttachmentQuickPickItem(pick)) { - if (pick.kind === 'folder-search-result') { - toAttach.push({ - kind: 'directory', - id: pick.id, - value: pick.resource, - name: basename(pick.resource), - }); - } else if (pick.kind === 'diagnostic-filter') { - toAttach.push({ - id: pick.id, - name: pick.label, - value: pick.filter, - kind: 'diagnostic', - icon: pick.icon, - ...pick.filter, - }); - - } else if (pick.kind === 'open-editors') { - for (const editor of editorService.editors.filter(e => e instanceof FileEditorInput || e instanceof DiffEditorInput || e instanceof UntitledTextEditorInput || e instanceof NotebookEditorInput)) { - const uri = editor instanceof DiffEditorInput ? editor.modified.resource : editor.resource; - if (uri) { - toAttach.push({ - kind: 'file', - id: this._getFileContextId({ resource: uri }), - value: uri, - name: labelService.getUriBasenameLabel(uri), - }); - } - } - } else if (pick.kind === 'search-results') { - const searchView = viewsService.getViewWithId(SEARCH_VIEW_ID) as SearchView; - for (const result of searchView.model.searchResult.matches()) { - toAttach.push({ - kind: 'file', - id: this._getFileContextId({ resource: result.resource }), - value: result.resource, - name: labelService.getUriBasenameLabel(result.resource), - }); - } - } else if (pick.kind === 'related-files') { - // Get all provider results and show them in a second tier picker - const chatSessionId = widget.viewModel?.sessionId; - if (!chatSessionId || !chatEditingService) { - continue; - } - const relatedFiles = await chatEditingService.getRelatedFiles(chatSessionId, widget.getInput(), widget.attachmentModel.fileAttachments, CancellationToken.None); - if (!relatedFiles) { - continue; - } - const attachments = widget.attachmentModel.getAttachmentIDs(); - const itemsPromise = chatEditingService.getRelatedFiles(chatSessionId, widget.getInput(), widget.attachmentModel.fileAttachments, CancellationToken.None) - .then((files) => (files ?? []).reduce<(WithUriValue | IQuickPickSeparator)[]>((acc, cur) => { - acc.push({ type: 'separator', label: cur.group }); - for (const file of cur.files) { - acc.push({ - type: 'item', - label: labelService.getUriBasenameLabel(file.uri), - description: labelService.getUriLabel(dirname(file.uri), { relative: true }), - value: file.uri, - disabled: attachments.has(this._getFileContextId({ resource: file.uri })), - picked: true - }); - } - return acc; - }, [])); - const selectedFile = await quickInputService.pick(itemsPromise, { placeHolder: localize('relatedFiles', 'Add related files to your working set') }); - if (selectedFile) { - toAttach.push({ - kind: 'file', - id: this._getFileContextId({ resource: selectedFile.value }), - value: selectedFile.value, - name: selectedFile.label, - omittedState: OmittedState.NotOmitted - }); - } - } else if (pick.kind === 'screenshot') { - const blob = await hostService.getScreenshot(); - if (blob) { - toAttach.push(convertBufferToScreenshotVariable(blob)); - } - } else if (pick.kind === 'command') { - // Dynamic variable with a followup command - const selection = await commandService.executeCommand(pick.command.id, ...(pick.command.arguments ?? [])); - if (!selection) { - // User made no selection, skip this variable - continue; - } - toAttach.push({ - ...pick, - value: pick.value, - name: `${typeof pick.value === 'string' && pick.value.startsWith('#') ? pick.value.slice(1) : ''}${selection}`, - // Apply the original icon with the new name - fullName: selection - }); - } else if (pick.kind === 'tool') { - toAttach.push({ - id: pick.id, - name: pick.tool.displayName, - fullName: pick.tool.displayName, - value: undefined, - icon: pick.icon, - kind: 'tool' - }); - } else if (pick.kind === 'image') { - const fileBuffer = await clipboardService.readImage(); - toAttach.push({ - id: await imageToHash(fileBuffer), - name: localize('pastedImage', 'Pasted Image'), - fullName: localize('pastedImage', 'Pasted Image'), - value: fileBuffer, - kind: 'image', - }); - } - } else if (isISymbolQuickPickItem(pick) && pick.symbol) { - // Workspace symbol - toAttach.push({ - kind: 'symbol', - id: this._getFileContextId(pick.symbol.location), - value: pick.symbol.location, - symbolKind: pick.symbol.kind, - icon: SymbolKinds.toIcon(pick.symbol.kind), - fullName: pick.label, - name: pick.symbol.name, - }); - } else if (isIQuickPickItemWithResource(pick) && pick.resource) { - if (/\.(png|jpg|jpeg|bmp|gif|tiff)$/i.test(pick.resource.path)) { - // checks if the file is an image - if (URI.isUri(pick.resource)) { - // read the image and attach a new file context. - const readFile = await fileService.readFile(pick.resource); - const resizedImage = await resizeImage(readFile.value.buffer); - toAttach.push({ - id: pick.resource.toString(), - name: pick.label, - fullName: pick.label, - value: resizedImage, - kind: 'image', - references: [{ reference: pick.resource, kind: 'reference' }] - }); - } - } else { - let omittedState = OmittedState.NotOmitted; - try { - const createdModel = await textModelService.createModelReference(pick.resource); - createdModel.dispose(); - } catch { - omittedState = OmittedState.Full; - } - - toAttach.push({ - kind: 'file', - id: this._getFileContextId({ resource: pick.resource }), - value: pick.resource, - name: pick.label, - omittedState - }); - } - } else if (isIGotoSymbolQuickPickItem(pick) && pick.uri && pick.range) { - toAttach.push({ - kind: 'generic', - id: this._getFileContextId({ uri: pick.uri, range: pick.range.decoration }), - value: { uri: pick.uri, range: pick.range.decoration }, - fullName: pick.label, - name: pick.symbolName!, - }); - } - } - - widget.attachmentModel.addContext(...toAttach); - if (!isInBackground) { - // Set focus back into the input once the user is done attaching items - // so that the user can start typing their message - widget.focusInput(); - } - } - override async run(accessor: ServicesAccessor, ...args: any[]): Promise { - const chatAgentService = accessor.get(IChatAgentService); - const widgetService = accessor.get(IChatWidgetService); - const clipboardService = accessor.get(IClipboardService); - const editorService = accessor.get(IEditorService); - const contextKeyService = accessor.get(IContextKeyService); + const instantiationService = accessor.get(IInstantiationService); + const widgetService = accessor.get(IChatWidgetService); + const contextKeyService = accessor.get(IContextKeyService); const keybindingService = accessor.get(IKeybindingService); - const chatEditingService = accessor.get(IChatEditingService); + const contextPickService = accessor.get(IChatContextPickService); const context: { widget?: IChatWidget; placeholder?: string } | undefined = args[0]; const widget = context?.widget ?? widgetService.lastFocusedWidget; @@ -683,245 +327,62 @@ export class AttachContextAction extends Action2 { return; } - const quickPickItems: IAttachmentQuickPickItem[] = []; - if (widget.input.selectedLanguageModel?.metadata.capabilities?.vision) { - const imageData = await clipboardService.readImage(); - if (isImage(imageData)) { - quickPickItems.push({ - kind: 'image', - id: await imageToHash(imageData), - label: localize('imageFromClipboard', 'Image from Clipboard'), - iconClass: ThemeIcon.asClassName(Codicon.fileMedia), - }); + const quickPickItems: IContextPickItemItem[] = []; + + for (const item of contextPickService.items) { + + if (item.isEnabled && !await item.isEnabled(widget)) { + continue; } quickPickItems.push({ - kind: 'screenshot', - id: ScreenshotVariableId, - icon: ThemeIcon.fromId(Codicon.deviceCamera.id), - iconClass: ThemeIcon.asClassName(Codicon.deviceCamera), - label: (isElectron - ? localize('chatContext.attachScreenshot.labelElectron.Window', 'Screenshot Window') - : localize('chatContext.attachScreenshot.labelWeb', 'Screenshot')), + kind: 'contextPick', + item, + label: item.label, + iconClass: ThemeIcon.asClassName(item.icon), + keybinding: item.commandId ? keybindingService.lookupKeybinding(item.commandId, contextKeyService) : undefined, }); } - if (widget.viewModel?.sessionId) { - const agentPart = widget.parsedInput.parts.find((part): part is ChatRequestAgentPart => part instanceof ChatRequestAgentPart); - if (agentPart) { - const completions = await chatAgentService.getAgentCompletionItems(agentPart.agent.id, '', CancellationToken.None); - for (const variable of completions) { - if (variable.fullName && variable.command) { - quickPickItems.push({ - kind: 'command', - label: variable.fullName, - id: variable.id, - command: variable.command, - icon: variable.icon, - iconClass: variable.icon ? ThemeIcon.asClassName(variable.icon) : undefined, - value: variable.value, - name: variable.name - }); - } else { - // Currently there's nothing that falls into this category - } - } - } - } - - quickPickItems.push({ - kind: 'tools', - label: localize('chatContext.tools', 'Tools...'), - iconClass: ThemeIcon.asClassName(Codicon.tools), - id: 'tools', - }); - - quickPickItems.push({ - kind: 'workspaceSymbol', - label: localize('chatContext.symbol', 'Symbols...'), - iconClass: ThemeIcon.asClassName(Codicon.symbolField), - id: 'symbol' - }); - - quickPickItems.push({ - kind: 'folder', - label: localize('chatContext.folder', 'Files & Folders...'), - iconClass: ThemeIcon.asClassName(Codicon.folder), - id: 'folder', - }); - - quickPickItems.push({ - kind: 'diagnostic', - label: localize('chatContext.diagnstic', 'Problems...'), - iconClass: ThemeIcon.asClassName(Codicon.error), - id: 'diagnostic' - }); - - if (widget.location === ChatAgentLocation.Notebook) { - quickPickItems.push({ - kind: 'command', - id: 'chatContext.notebook.kernelVariable', - icon: ThemeIcon.fromId(Codicon.serverEnvironment.id), - iconClass: ThemeIcon.asClassName(Codicon.serverEnvironment), - value: 'kernelVariable', - label: localize('chatContext.notebook.kernelVariable', 'Kernel Variable...'), - command: { - id: 'notebook.chat.selectAndInsertKernelVariable', - title: localize('chatContext.notebook.selectkernelVariable', 'Select and Insert Kernel Variable'), - arguments: [{ widget, range: undefined }] - } - }); - } - - if (chatEditingService?.hasRelatedFilesProviders() && (widget.getInput() || widget.attachmentModel.fileAttachments.length > 0)) { - quickPickItems.push({ - kind: 'related-files', - id: 'related-files', - label: localize('chatContext.relatedFiles', 'Related Files'), - iconClass: ThemeIcon.asClassName(Codicon.sparkle), - }); - } - if (editorService.editors.filter(e => e instanceof FileEditorInput || e instanceof DiffEditorInput || e instanceof UntitledTextEditorInput).length > 0) { - quickPickItems.push({ - kind: 'open-editors', - id: 'open-editors', - label: localize('chatContext.editors', 'Open Editors'), - iconClass: ThemeIcon.asClassName(Codicon.files), - }); - } - if (SearchContext.HasSearchResults.getValue(contextKeyService)) { - quickPickItems.push({ - kind: 'search-results', - id: 'search-results', - label: localize('chatContext.searchResults', 'Search Results'), - iconClass: ThemeIcon.asClassName(Codicon.search), - }); - } - - // if the `reusable prompts` feature is enabled, add - // the appropriate attachment type to the list - if (widget.attachmentModel.promptInstructions.featureEnabled) { - const keybinding = keybindingService.lookupKeybinding(INSTRUCTIONS_COMMAND_ID, contextKeyService); - - quickPickItems.push({ - id: INSTRUCTION_PICK_ID, - kind: INSTRUCTION_PICK_ID, - label: localize('chatContext.attach.instructions.label', 'Instructions...'), - iconClass: ThemeIcon.asClassName(Codicon.bookmark), - keybinding, - }); - } - - quickPickItems.sort((a, b) => { - let result = attachmentsOrdinals.indexOf(b.kind) - attachmentsOrdinals.indexOf(a.kind); - if (result === 0) { - result = a.label.localeCompare(b.label); - } - return result; - }); - - instantiationService.invokeFunction(this._show.bind(this), widget, quickPickItems, '', context?.placeholder); + instantiationService.invokeFunction(this._show.bind(this), widget, quickPickItems, context?.placeholder); } - private async _showDiagnosticsPick(instantiationService: IInstantiationService, onBackgroundAccept: (item: IChatContextQuickPickItem[]) => void): Promise { - const convert = (item: IDiagnosticVariableEntryFilterData): IDiagnosticsQuickPickItemWithFilter => ({ - kind: 'diagnostic-filter', - id: IDiagnosticVariableEntryFilterData.id(item), - label: IDiagnosticVariableEntryFilterData.label(item), - icon: IDiagnosticVariableEntryFilterData.icon, - filter: item, - }); - - const filter = await instantiationService.invokeFunction(createMarkersQuickPick, items => onBackgroundAccept(items.map(convert))); - return filter && convert(filter); - } - - private _show(accessor: ServicesAccessor, widget: IChatWidget, quickPickItems: (IChatContextQuickPickItem | QuickPickItem)[] | undefined, query: string = '', placeholder?: string) { + private _show(accessor: ServicesAccessor, widget: IChatWidget, additionPicks: IContextPickItemItem[] | undefined, placeholder?: string) { const quickInputService = accessor.get(IQuickInputService); const quickChatService = accessor.get(IQuickChatService); - const editorService = accessor.get(IEditorService); - const commandService = accessor.get(ICommandService); const instantiationService = accessor.get(IInstantiationService); - const attach = (isBackgroundAccept: boolean, ...items: IChatContextQuickPickItem[]) => { - instantiationService.invokeFunction(this._attachContext.bind(this), widget, isBackgroundAccept, ...items); - }; const providerOptions: AnythingQuickAccessProviderRunOptions = { - additionPicks: quickPickItems, - handleAccept: async (inputItem: IChatContextQuickPickItem, isBackgroundAccept: boolean) => { - let item: IChatContextQuickPickItem | undefined = inputItem; + additionPicks, + handleAccept: async (item: IQuickPickServicePickItem | IContextPickItemItem, isBackgroundAccept: boolean) => { - if (isIAttachmentQuickPickItem(item)) { + if (isIContextPickItemItem(item)) { - if (item.kind === 'workspaceSymbol') { - instantiationService.invokeFunction(this._show.bind(this), widget, quickPickItems, SymbolsQuickAccessProvider.PREFIX, placeholder); - return; - } else if (item.kind === 'instructions') { - runAttachInstructionsAction(commandService, { widget }); - return; + let isDone = true; + if (item.item.type === 'valuePick') { + this._handleContextPick(item.item, widget); + + } else if (item.item.type === 'pickerPick') { + isDone = await this._handleContextPickerItem(quickInputService, item.item, widget); } - if (item.kind === 'folder') { - item = await this._showFolders(instantiationService); - } else if (item.kind === 'diagnostic') { - item = await this._showDiagnosticsPick(instantiationService, i => attach(true, ...i)); - } else if (item.kind === 'tools') { - item = await instantiationService.invokeFunction(showToolsPick, widget); - } - if (!item) { + if (!isDone) { // restart picker when sub-picker didn't return anything - instantiationService.invokeFunction(this._show.bind(this), widget, quickPickItems, '', placeholder); + instantiationService.invokeFunction(this._show.bind(this), widget, additionPicks, placeholder); return; } + } else { + instantiationService.invokeFunction(this._handleQPPick.bind(this), widget, isBackgroundAccept, item); } - attach(isBackgroundAccept, item); if (isQuickChat(widget)) { quickChatService.open(); } - - }, - filter: (item: IChatContextQuickPickItem | IQuickPickSeparator) => { - // Avoid attaching the same context twice - const attachedContext = widget.attachmentModel.getAttachmentIDs(); - - if (isIAttachmentQuickPickItem(item) && item.kind === 'open-editors') { - for (const editor of editorService.editors.filter(e => e instanceof FileEditorInput || e instanceof DiffEditorInput || e instanceof UntitledTextEditorInput)) { - // There is an open editor that hasn't yet been attached to the chat - if (editor.resource && !attachedContext.has(this._getFileContextId({ resource: editor.resource }))) { - return true; - } - } - return false; - } - - if ('kind' in item && item.kind === 'image') { - return !attachedContext.has(item.id); - } - - if ('symbol' in item && item.symbol) { - return !attachedContext.has(this._getFileContextId(item.symbol.location)); - } - - if (item && typeof item === 'object' && 'resource' in item && URI.isUri(item.resource)) { - return [Schemas.file, Schemas.vscodeRemote, Schemas.untitled].includes(item.resource.scheme) - && !attachedContext.has(this._getFileContextId({ resource: item.resource })); // Hack because Typescript doesn't narrow this type correctly - } - - if (item && typeof item === 'object' && 'uri' in item && item.uri && item.range) { - return !attachedContext.has(this._getFileContextId({ uri: item.uri, range: item.range.decoration })); - } - - if (!('command' in item) && item.id) { - return !attachedContext.has(item.id); - } - - // Don't filter out dynamic variables which show secondary data (temporary) - return true; } }; - quickInputService.quickAccess.show(query, { + + quickInputService.quickAccess.show('', { enabledProviderPrefixes: [ AnythingQuickAccessProvider.PREFIX, SymbolsQuickAccessProvider.PREFIX, @@ -932,140 +393,153 @@ export class AttachContextAction extends Action2 { }); } - private async _showFolders(instantiationService: IInstantiationService): Promise { - const folder = await instantiationService.invokeFunction(createFilesAndFolderQuickPick); - if (!folder) { - return undefined; - } + private async _handleQPPick(accessor: ServicesAccessor, widget: IChatWidget, isInBackground: boolean, pick: IQuickPickServicePickItem) { + const fileService = accessor.get(IFileService); + const textModelService = accessor.get(ITextModelService); - return { - kind: 'folder-search-result', - id: folder.toString(), - label: basename(folder), - resource: folder, - }; - } -} -async function createMarkersQuickPick(accessor: ServicesAccessor, onBackgroundAccept?: (item: IDiagnosticVariableEntryFilterData[]) => void): Promise { - const quickInputService = accessor.get(IQuickInputService); - const markerService = accessor.get(IMarkerService); - const labelService = accessor.get(ILabelService); + const toAttach: IChatRequestVariableEntry[] = []; - const markers = markerService.read({ severities: MarkerSeverity.Error | MarkerSeverity.Warning | MarkerSeverity.Info }); - const grouped = groupBy(markers, (a, b) => extUri.compare(a.resource, b.resource)); + if (isIQuickPickItemWithResource(pick) && pick.resource) { + if (/\.(png|jpg|jpeg|bmp|gif|tiff)$/i.test(pick.resource.path)) { + // checks if the file is an image + if (URI.isUri(pick.resource)) { + // read the image and attach a new file context. + const readFile = await fileService.readFile(pick.resource); + const resizedImage = await resizeImage(readFile.value.buffer); + toAttach.push({ + id: pick.resource.toString(), + name: pick.label, + fullName: pick.label, + value: resizedImage, + kind: 'image', + references: [{ reference: pick.resource, kind: 'reference' }] + }); + } + } else { + let omittedState = OmittedState.NotOmitted; + try { + const createdModel = await textModelService.createModelReference(pick.resource); + createdModel.dispose(); + } catch { + omittedState = OmittedState.Full; + } - const severities = new Set(); - type MarkerPickItem = IQuickPickItem & { resource?: URI; entry: IDiagnosticVariableEntryFilterData }; - const items: (MarkerPickItem | IQuickPickSeparator)[] = []; - - let pickCount = 0; - for (const group of grouped) { - const resource = group[0].resource; - - items.push({ type: 'separator', label: labelService.getUriLabel(resource, { relative: true }) }); - for (const marker of group) { - pickCount++; - severities.add(marker.severity); - items.push({ - type: 'item', - resource: marker.resource, - label: marker.message, - description: localize('markers.panel.at.ln.col.number', "[Ln {0}, Col {1}]", '' + marker.startLineNumber, '' + marker.startColumn), - entry: IDiagnosticVariableEntryFilterData.fromMarker(marker), + toAttach.push({ + kind: 'file', + id: pick.resource.toString(), + value: pick.resource, + name: pick.label, + omittedState + }); + } + } else if (isIGotoSymbolQuickPickItem(pick) && pick.uri && pick.range) { + toAttach.push({ + kind: 'generic', + id: JSON.stringify({ uri: pick.uri, range: pick.range.decoration }), + value: { uri: pick.uri, range: pick.range.decoration }, + fullName: pick.label, + name: pick.symbolName!, }); } + + + widget.attachmentModel.addContext(...toAttach); + + if (!isInBackground) { + // Set focus back into the input once the user is done attaching items + // so that the user can start typing their message + widget.focusInput(); + } } - items.unshift({ type: 'item', label: localize('markers.panel.allErrors', 'All Problems'), entry: { filterSeverity: MarkerSeverity.Info } }); + private async _handleContextPick(item: IChatContextValueItem, widget: IChatWidget) { - const store = new DisposableStore(); - const quickPick = store.add(quickInputService.createQuickPick({ useSeparators: true })); - quickPick.canAcceptInBackground = !onBackgroundAccept; - quickPick.placeholder = localize('pickAProblem', 'Select a problem to attach'); - quickPick.items = items; + const value = await item.asAttachment(widget); + if (Array.isArray(value)) { + widget.attachmentModel.addContext(...value); + } else if (value) { + widget.attachmentModel.addContext(value); + } + } - return new Promise(resolve => { - store.add(quickPick.onDidHide(() => resolve(undefined))); - store.add(quickPick.onDidAccept(ev => { - if (ev.inBackground) { - onBackgroundAccept?.(quickPick.selectedItems.map(i => i.entry)); - } else { - resolve(quickPick.selectedItems[0]?.entry); - quickPick.dispose(); + private async _handleContextPickerItem(quickInputService: IQuickInputService, item: IChatContextPickerItem, widget: IChatWidget): Promise { + + const pickerConfig = item.asPicker(widget); + + const store = new DisposableStore(); + + const goBackItem: IQuickPickItem = { + label: localize('goBack', 'Go back ↩'), + alwaysShow: true + }; + const extraPicks: QuickPickItem[] = [{ type: 'separator' }, goBackItem]; + + const qp = store.add(quickInputService.createQuickPick({ useSeparators: true })); + + qp.placeholder = pickerConfig.placeholder; + qp.matchOnDescription = true; + // qp.ignoreFocusOut = true; + qp.canAcceptInBackground = true; + qp.busy = true; + qp.show(); + + if (isThenable(pickerConfig.picks)) { + const items = await (pickerConfig.picks.then(value => { + return ([] as QuickPickItem[]).concat(value, extraPicks); + })); + + qp.items = items; + qp.busy = false; + } else { + + let cts: CancellationTokenSource | undefined; + + const update = async () => { + assertType(typeof pickerConfig.picks === 'function'); + + if (cts) { + cts.cancel(); + store.delete(cts); + } + cts = store.add(new CancellationTokenSource()); + + try { + qp.busy = true; + const items = await raceCancellationError(pickerConfig.picks(qp.value, cts.token), cts.token); + qp.items = ([] as QuickPickItem[]).concat(items, extraPicks); + } finally { + qp.busy = false; + } + }; + + store.add(qp.onDidChangeValue(update)); + update(); + } + + const defer = new DeferredPromise(); + + store.add(qp.onDidAccept(e => { + const [selected] = qp.selectedItems; + if (isChatContextPickerPickItem(selected)) { + widget.attachmentModel.addContext(selected.asAttachment()); + } + if (selected === goBackItem) { + defer.complete(false); + } + if (!e.inBackground) { + defer.complete(true); } })); - quickPick.show(); - }).finally(() => store.dispose()); + + store.add(qp.onDidHide(() => { + defer.complete(true); + })); + + try { + return await defer.p; + } finally { + store.dispose(); + } + } } - -async function showToolsPick(accessor: ServicesAccessor, widget: IChatWidget): Promise { - - const quickPickService = accessor.get(IQuickInputService); - - - function classify(tool: IToolData) { - if (tool.source.type === 'internal' || tool.source.type === 'extension' && !tool.source.isExternalTool) { - return { ordinal: 1, groupLabel: localize('chatContext.tools.internal', 'Built-In') }; - } else if (tool.source.type === 'mcp') { - return { ordinal: 2, groupLabel: localize('chatContext.tools.mcp', 'MCP Servers') }; - } else { - return { ordinal: 3, groupLabel: localize('chatContext.tools.extension', 'Extensions') }; - } - } - - type Pick = IToolQuickPickItem & { ordinal: number; groupLabel: string }; - const items: Pick[] = []; - - for (const tool of widget.input.selectedToolsModel.tools.get()) { - if (!tool.canBeReferencedInPrompt) { - continue; - } - const item: Pick = { - tool, - ...classify(tool), - kind: 'tool', - label: tool.toolReferenceName ?? tool.id, - description: (tool.toolReferenceName ?? tool.id) !== tool.displayName ? tool.displayName : undefined, - id: tool.id, - }; - // if (ThemeIcon.isThemeIcon(tool.icon)) { - // item.iconClass = ThemeIcon.asClassName(tool.icon); - // } else if (tool.icon) { - // item.iconPath = tool.icon; - // } - items.push(item); - } - - items.sort((a, b) => { - let res = a.ordinal - b.ordinal; - if (res === 0) { - res = a.label.localeCompare(b.label); - } - return res; - }); - - let lastGroupLabel: string | undefined; - const picks: (IQuickPickSeparator | Pick)[] = []; - - - for (const item of items) { - if (lastGroupLabel !== item.groupLabel) { - picks.push({ type: 'separator', label: item.groupLabel }); - lastGroupLabel = item.groupLabel; - } - picks.push(item); - } - - const result = await quickPickService.pick(picks, { - placeHolder: localize('chatContext.tools.placeholder', 'Select a tool'), - canPickMany: false - }); - - return result; -} - -/** - * Register all actions related to reusable prompt files. - */ -registerPromptActions(); diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatDeveloperActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatDeveloperActions.ts index 11420ee03b7..b533b006bfb 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatDeveloperActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatDeveloperActions.ts @@ -46,7 +46,8 @@ class LogChatIndexAction extends Action2 { title: localize2('workbench.action.chat.logChatIndex.label', "Log Chat Index"), icon: Codicon.attach, category: Categories.Developer, - f1: true + f1: true, + precondition: ChatContextKeys.enabled }); } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index 4ab605c6a73..f53c25ba5ca 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -117,7 +117,7 @@ class ToggleChatModeAction extends Action2 { }, menu: [ { - id: MenuId.ChatExecute, + id: MenuId.ChatInput, order: 1, when: ContextKeyExpr.and( ChatContextKeys.enabled, @@ -251,7 +251,7 @@ class OpenModelPickerAction extends Action2 { }, precondition: ChatContextKeys.enabled, menu: { - id: MenuId.ChatExecute, + id: MenuId.ChatInput, order: 3, group: 'navigation', when: ContextKeyExpr.and( diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatMoveActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatMoveActions.ts index 3caa15f12e5..c1560b58df6 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatMoveActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatMoveActions.ts @@ -3,11 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Codicon } from '../../../../../base/common/codicons.js'; import { localize2 } from '../../../../../nls.js'; import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js'; import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; -import { ActiveEditorContext } from '../../../../common/contextkeys.js'; +import { ActiveEditorContext, IsAuxiliaryTitleBarContext, IsCompactTitleBarContext } from '../../../../common/contextkeys.js'; import { IEditorGroupsService } from '../../../../services/editor/common/editorGroupsService.js'; import { ACTIVE_GROUP, AUX_WINDOW_GROUP, IEditorService } from '../../../../services/editor/common/editorService.js'; import { IViewsService } from '../../../../services/views/common/viewsService.js'; @@ -78,12 +79,21 @@ export function registerMoveActions() { id: `workbench.action.chat.openInSidebar`, title: localize2('interactiveSession.openInSidebar.label', "Open Chat in Side Bar"), category: CHAT_CATEGORY, + icon: Codicon.layoutSidebarRight, precondition: ChatContextKeys.enabled, f1: true, menu: [{ id: MenuId.EditorTitle, order: 0, when: ActiveEditorContext.isEqualTo(ChatEditorInput.EditorID), + }, { + id: MenuId.LayoutControlMenu, + when: ContextKeyExpr.and( + ActiveEditorContext.isEqualTo(ChatEditorInput.EditorID), + IsAuxiliaryTitleBarContext, + IsCompactTitleBarContext + ), + order: -2 }] }); } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatToolActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatToolActions.ts index e574f8e143e..d67aeebb55a 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatToolActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatToolActions.ts @@ -95,9 +95,9 @@ export class AttachToolsAction extends Action2 { precondition: ChatContextKeys.chatMode.isEqualTo(ChatMode.Agent), menu: { when: ChatContextKeys.chatMode.isEqualTo(ChatMode.Agent), - id: MenuId.ChatInput, + id: MenuId.ChatExecute, group: 'navigation', - order: 100 + order: 1, }, keybinding: { when: ContextKeyExpr.and(ChatContextKeys.inChatInput, ChatContextKeys.chatMode.isEqualTo(ChatMode.Agent)), @@ -177,7 +177,7 @@ export class AttachToolsAction extends Action2 { const toolBuckets = new Map(); for (const tool of toolsService.getTools()) { - if (!tool.supportsToolPicker) { + if (!tool.canBeReferencedInPrompt) { continue; } diff --git a/src/vs/workbench/contrib/chat/browser/actions/promptActions/chatAttachInstructionsAction.ts b/src/vs/workbench/contrib/chat/browser/actions/promptActions/chatAttachInstructionsAction.ts index 905fe400340..67f32780dd2 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/promptActions/chatAttachInstructionsAction.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/promptActions/chatAttachInstructionsAction.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IChatWidget } from '../../chat.js'; +import { IChatWidget, IChatWidgetService } from '../../chat.js'; import { CHAT_CATEGORY } from '../chatActions.js'; import { URI } from '../../../../../../base/common/uri.js'; import { localize, localize2 } from '../../../../../../nls.js'; @@ -16,9 +16,22 @@ import { PromptFilePickers } from './dialogs/askToSelectPrompt/promptFilePickers import { ServicesAccessor } from '../../../../../../editor/browser/editorExtensions.js'; import { ICommandService } from '../../../../../../platform/commands/common/commands.js'; import { ContextKeyExpr } from '../../../../../../platform/contextkey/common/contextkey.js'; -import { Action2, registerAction2 } from '../../../../../../platform/actions/common/actions.js'; +import { Action2, MenuId, registerAction2 } from '../../../../../../platform/actions/common/actions.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { attachInstructionsFiles, IAttachOptions } from './dialogs/askToSelectPrompt/utils/attachInstructions.js'; +import { IChatContextPickerItem, IChatContextPickerPickItem } from '../../chatContextPickService.js'; +import { CancellationToken } from '../../../../../../base/common/cancellation.js'; +import { IQuickPickSeparator } from '../../../../../../platform/quickinput/common/quickInput.js'; +import { Codicon } from '../../../../../../base/common/codicons.js'; +import { getCleanPromptName } from '../../../../../../platform/prompts/common/constants.js'; +import { compare } from '../../../../../../base/common/strings.js'; +import { ILabelService } from '../../../../../../platform/label/common/label.js'; +import { dirname } from '../../../../../../base/common/resources.js'; +import { IPromptFileVariableEntry } from '../../../common/chatModel.js'; +import { KeyMod, KeyCode } from '../../../../../../base/common/keyCodes.js'; +import { KeybindingWeight } from '../../../../../../platform/keybinding/common/keybindingsRegistry.js'; +import { ICodeEditorService } from '../../../../../../editor/browser/services/codeEditorService.js'; +import { INSTRUCTIONS_LANGUAGE_ID } from '../../../common/promptSyntax/constants.js'; /** * Action ID for the `Attach Instruction` action. @@ -64,18 +77,33 @@ class AttachInstructionsAction extends Action2 { f1: false, precondition: ContextKeyExpr.and(PromptsConfig.enabledCtx, ChatContextKeys.enabled), category: CHAT_CATEGORY, + keybinding: { + primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.Slash, + weight: KeybindingWeight.WorkbenchContrib + }, + menu: { + id: MenuId.CommandPalette, + when: ContextKeyExpr.and(PromptsConfig.enabledCtx, ChatContextKeys.enabled) + } }); } public override async run( accessor: ServicesAccessor, - options: IAttachInstructionsActionOptions, + options?: IAttachInstructionsActionOptions, ): Promise { const viewsService = accessor.get(IViewsService); const promptsService = accessor.get(IPromptsService); const commandService = accessor.get(ICommandService); const instaService = accessor.get(IInstantiationService); + if (!options) { + options = { + resource: getActiveInstructionsFileUri(accessor), + widget: getFocusedChatWidget(accessor), + }; + } + const pickers = instaService.createInstance(PromptFilePickers); const { skipSelectionDialog, resource } = options; @@ -86,7 +114,7 @@ class AttachInstructionsAction extends Action2 { commandService, }; - if (skipSelectionDialog === true) { + if (skipSelectionDialog) { assertDefined( resource, 'Resource must be defined when skipping prompt selection dialog.', @@ -121,19 +149,32 @@ class AttachInstructionsAction extends Action2 { } } +function getFocusedChatWidget(accessor: ServicesAccessor): IChatWidget | undefined { + const chatWidgetService = accessor.get(IChatWidgetService); + + const { lastFocusedWidget } = chatWidgetService; + if (!lastFocusedWidget) { + return undefined; + } + + // the widget input `must` be focused at the time when command run + if (!lastFocusedWidget.hasInputFocus()) { + return undefined; + } + + return lastFocusedWidget; +} + /** - * Runs the `Attach Instructions` action with provided options. We export this - * function instead of {@link ATTACH_INSTRUCTIONS_ACTION_ID} directly to - * encapsulate/enforce the correct options to be passed to the action. + * Gets `URI` of a instructions file open in an active editor instance, if any. */ -export const runAttachInstructionsAction = async ( - commandService: ICommandService, - options: IAttachInstructionsActionOptions, -): Promise => { - return await commandService.executeCommand( - ATTACH_INSTRUCTIONS_ACTION_ID, - options, - ); +const getActiveInstructionsFileUri = (accessor: ServicesAccessor): URI | undefined => { + const codeEditorService = accessor.get(ICodeEditorService); + const model = codeEditorService.getActiveCodeEditor()?.getModel(); + if (model?.getLanguageId() === INSTRUCTIONS_LANGUAGE_ID) { + return model.uri; + } + return undefined; }; /** @@ -142,3 +183,65 @@ export const runAttachInstructionsAction = async ( export const registerAttachPromptActions = () => { registerAction2(AttachInstructionsAction); }; + + +export class ChatInstructionsPickerPick implements IChatContextPickerItem { + + readonly type = 'pickerPick'; + readonly label = localize('chatContext.attach.instructions.label', 'Instructions...'); + readonly icon = Codicon.bookmark; + readonly commandId = ATTACH_INSTRUCTIONS_ACTION_ID; + + constructor( + @IPromptsService private readonly promptsService: IPromptsService, + @ILabelService private readonly labelService: ILabelService + ) { } + + isEnabled(widget: IChatWidget): Promise | boolean { + return widget.attachmentModel.promptInstructions.featureEnabled; + } + + asPicker(): { readonly placeholder: string; readonly picks: Promise<(IChatContextPickerPickItem | IQuickPickSeparator)[]> | ((query: string, token: CancellationToken) => Promise<(IChatContextPickerPickItem | IQuickPickSeparator)[]>) } { + + const picks = this.promptsService.listPromptFiles('instructions').then(value => { + + const result: (IChatContextPickerPickItem | IQuickPickSeparator)[] = []; + + value = value.slice(0).sort((a, b) => compare(a.storage, b.storage)); + + let storageType: string | undefined; + + for (const { uri, storage } of value) { + + if (storageType !== storage) { + storageType = storage; + result.push({ + type: 'separator', + label: storage === 'user' + ? localize('user-data-dir.capitalized', 'User data folder') + : this.labelService.getUriLabel(dirname(uri), { relative: true }) + }); + } + + result.push({ + label: getCleanPromptName(uri), + asAttachment: (): IPromptFileVariableEntry => { + return { + kind: 'promptFile', + id: uri.toString(), + value: uri, + name: this.labelService.getUriBasenameLabel(uri), + }; + } + }); + } + return result; + }); + + return { + placeholder: localize('placeholder', 'Select instructions files to attach'), + picks + }; + } + +} diff --git a/src/vs/workbench/contrib/chat/browser/actions/promptActions/index.ts b/src/vs/workbench/contrib/chat/browser/actions/promptActions/index.ts index 629a85d1ee8..88c73037595 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/promptActions/index.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/promptActions/index.ts @@ -6,7 +6,6 @@ import { registerRunPromptActions } from './chatRunPromptAction.js'; import { registerSaveToPromptActions } from './chatSaveToPromptAction.js'; import { registerAttachPromptActions } from './chatAttachInstructionsAction.js'; -export { runAttachInstructionsAction } from './chatAttachInstructionsAction.js'; /** * Helper to register all actions related to reusable prompt files. diff --git a/src/vs/workbench/contrib/chat/browser/attachments/promptInstructions/promptInstructionsCollectionWidget.ts b/src/vs/workbench/contrib/chat/browser/attachments/promptInstructions/promptInstructionsCollectionWidget.ts index 43a3bf32651..ab0820b7bc8 100644 --- a/src/vs/workbench/contrib/chat/browser/attachments/promptInstructions/promptInstructionsCollectionWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/attachments/promptInstructions/promptInstructionsCollectionWidget.ts @@ -6,11 +6,11 @@ import { URI } from '../../../../../../base/common/uri.js'; import { Emitter } from '../../../../../../base/common/event.js'; import { ResourceLabels } from '../../../../../browser/labels.js'; +import { Disposable } from '../../../../../../base/common/lifecycle.js'; import { ILogService } from '../../../../../../platform/log/common/log.js'; import { InstructionsAttachmentWidget } from './promptInstructionsWidget.js'; import { IModelService } from '../../../../../../editor/common/services/model.js'; import { INSTRUCTIONS_LANGUAGE_ID } from '../../../common/promptSyntax/constants.js'; -import { Disposable, IDisposable } from '../../../../../../base/common/lifecycle.js'; import { ILanguageService } from '../../../../../../editor/common/languages/language.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { ChatPromptAttachmentsCollection } from '../../chatAttachmentModel/chatPromptAttachmentsCollection.js'; @@ -32,12 +32,9 @@ export class PromptInstructionsAttachmentsCollectionWidget extends Disposable { */ private _onAttachmentsChange = this._register(new Emitter()); /** - * Subscribe to the `onAttachmentsChange` event. - * @param callback Function to invoke when number of attachments change. + * Subscribe to the event that fires when number of attachments change. */ - public onAttachmentsChange(callback: () => unknown): IDisposable { - return this._onAttachmentsChange.event(callback); - } + public readonly onAttachmentsChange = this._onAttachmentsChange.event; /** * The parent DOM node this widget was rendered into. @@ -89,18 +86,16 @@ export class PromptInstructionsAttachmentsCollectionWidget extends Disposable { constructor( private readonly model: ChatPromptAttachmentsCollection, private readonly resourceLabels: ResourceLabels, - @IInstantiationService private readonly initService: IInstantiationService, + @IInstantiationService private readonly instantiationService: IInstantiationService, @ILanguageService private readonly languageService: ILanguageService, @IModelService private readonly modelService: IModelService, @ILogService private readonly logService: ILogService, ) { super(); - this.render = this.render.bind(this); - // when a new attachment model is added, create a new child widget for it this._register(this.model.onAdd((attachment) => { - const widget = this.initService.createInstance( + const widget = this.instantiationService.createInstance( InstructionsAttachmentWidget, attachment, this.resourceLabels, diff --git a/src/vs/workbench/contrib/chat/browser/attachments/promptInstructions/promptInstructionsWidget.ts b/src/vs/workbench/contrib/chat/browser/attachments/promptInstructions/promptInstructionsWidget.ts index ba574de244a..363c28e22ea 100644 --- a/src/vs/workbench/contrib/chat/browser/attachments/promptInstructions/promptInstructionsWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/attachments/promptInstructions/promptInstructionsWidget.ts @@ -65,11 +65,8 @@ export class InstructionsAttachmentWidget extends ObservableDisposable { this.domNode = dom.$('.chat-prompt-attachment.chat-attached-context-attachment.show-file-icons.implicit'); - this.render = this.render.bind(this); - this.dispose = this.dispose.bind(this); - - this._register(this.model.onUpdate(this.render)); - this._register(this.model.onDispose(this.dispose)); + this._register(this.model.onUpdate(this.render.bind(this))); + this._register(this.model.onDispose(this.dispose.bind(this))); this.render(); } diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 44350505827..0b374285307 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -18,6 +18,7 @@ import { Extensions as ConfigurationExtensions, ConfigurationScope, IConfigurati import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { mcpGalleryServiceUrlConfig } from '../../../../platform/mcp/common/mcpManagement.js'; import { PromptsConfig } from '../../../../platform/prompts/common/config.js'; import { INSTRUCTIONS_DEFAULT_SOURCE_FOLDER, INSTRUCTION_FILE_EXTENSION, PROMPT_DEFAULT_SOURCE_FOLDER, PROMPT_FILE_EXTENSION } from '../../../../platform/prompts/common/constants.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; @@ -57,6 +58,7 @@ import { AgentChatAccessibilityHelp, EditsChatAccessibilityHelp, PanelChatAccess import { CopilotTitleBarMenuRendering, registerChatActions, ACTION_ID_NEW_CHAT } from './actions/chatActions.js'; import { registerNewChatActions } from './actions/chatClearActions.js'; import { CodeBlockActionRendering, registerChatCodeBlockActions, registerChatCodeCompareBlockActions } from './actions/chatCodeblockActions.js'; +import { ChatContextContributions } from './actions/chatContext.js'; import { registerChatContextActions } from './actions/chatContextActions.js'; import { registerChatCopyActions } from './actions/chatCopyActions.js'; import { registerChatDeveloperActions } from './actions/chatDeveloperActions.js'; @@ -74,6 +76,7 @@ import { IChatAccessibilityService, IChatCodeBlockContextProviderService, IChatW import { ChatAccessibilityService } from './chatAccessibilityService.js'; import './chatAttachmentModel.js'; import { ChatMarkdownAnchorService, IChatMarkdownAnchorService } from './chatContentParts/chatMarkdownAnchorService.js'; +import { ChatContextPickService, IChatContextPickService } from './chatContextPickService.js'; import { ChatInputBoxContentProvider } from './chatEdinputInputContentProvider.js'; import { ChatEditingEditorAccessibility } from './chatEditing/chatEditingEditorAccessibility.js'; import { registerChatEditorActions } from './chatEditing/chatEditingEditorActions.js'; @@ -100,7 +103,6 @@ import './contrib/chatInputEditorContrib.js'; import './contrib/chatInputEditorHover.js'; import { ChatRelatedFilesContribution } from './contrib/chatInputRelatedFilesContrib.js'; import { LanguageModelToolsService } from './languageModelToolsService.js'; -import './promptSyntax/contributions/attachInstructionsCommand.js'; import './promptSyntax/contributions/createPromptCommand/createPromptCommand.js'; import { ChatViewsWelcomeHandler } from './viewsWelcome/chatViewsWelcomeHandler.js'; @@ -267,6 +269,7 @@ configurationRegistry.registerConfiguration({ type: 'boolean', description: nls.localize('chat.extensionToolsEnabled', "Enable using tools contributed by third-party extensions."), default: true, + tags: ['preview'], policy: { name: 'ChatAgentExtensionTools', minimumVersion: '1.99', @@ -302,6 +305,18 @@ configurationRegistry.registerConfiguration({ default: true, markdownDescription: nls.localize('mpc.discovery.enabled', "Configures discovery of Model Context Protocol servers on the machine. It may be set to `true` or `false` to disable or enable all sources, and an mapping sources you wish to enable."), }, + [mcpGalleryServiceUrlConfig]: { + type: 'string', + description: nls.localize('mcp.gallery.serviceUrl', "Configure the MCP Gallery service URL to connect to"), + default: '', + scope: ConfigurationScope.APPLICATION, + tags: ['usesOnlineServices'], + included: false, + policy: { + name: 'McpGalleryServiceUrl', + minimumVersion: '1.101', + }, + }, [PromptsConfig.KEY]: { type: 'boolean', title: nls.localize( @@ -384,6 +399,12 @@ configurationRegistry.registerConfiguration({ }, ], }, + 'chat.setup.signInWithAlternateProvider': { + type: 'boolean', + description: nls.localize('chat.signInWithAlternateProvider', "Enable alternative sign-in provider."), + default: false, + tags: ['onExp', 'experimental'], + }, } }); Registry.as(EditorExtensions.EditorPane).registerEditorPane( @@ -617,6 +638,7 @@ registerWorkbenchContribution2(ChatEditingEditorOverlay.ID, ChatEditingEditorOve registerWorkbenchContribution2(SimpleBrowserOverlay.ID, SimpleBrowserOverlay, WorkbenchPhase.AfterRestored); registerWorkbenchContribution2(ChatEditingEditorContextKeys.ID, ChatEditingEditorContextKeys, WorkbenchPhase.AfterRestored); registerWorkbenchContribution2(ChatTransferContribution.ID, ChatTransferContribution, WorkbenchPhase.BlockRestore); +registerWorkbenchContribution2(ChatContextContributions.ID, ChatContextContributions, WorkbenchPhase.AfterRestored); registerChatActions(); registerChatCopyActions(); @@ -658,6 +680,7 @@ registerSingleton(IChatMarkdownAnchorService, ChatMarkdownAnchorService, Instant registerSingleton(ILanguageModelIgnoredFilesService, LanguageModelIgnoredFilesService, InstantiationType.Delayed); registerSingleton(IChatEntitlementService, ChatEntitlementService, InstantiationType.Delayed); registerSingleton(IPromptsService, PromptsService, InstantiationType.Delayed); +registerSingleton(IChatContextPickService, ChatContextPickService, InstantiationType.Delayed); registerWorkbenchContribution2(ChatEditingNotebookFileSystemProviderContrib.ID, ChatEditingNotebookFileSystemProviderContrib, WorkbenchPhase.BlockStartup); diff --git a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel.ts b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel.ts index 11d44a0edb1..f20a791b4da 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel.ts @@ -8,7 +8,7 @@ import { Emitter } from '../../../../base/common/event.js'; import { basename } from '../../../../base/common/resources.js'; import { IRange } from '../../../../editor/common/core/range.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; -import { IChatRequestVariableEntry } from '../common/chatModel.js'; +import { IChatRequestFileEntry, IChatRequestVariableEntry } from '../common/chatModel.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ChatPromptAttachmentsCollection } from './chatAttachmentModel/chatPromptAttachmentsCollection.js'; import { IFileService } from '../../../../platform/files/common/files.js'; @@ -18,6 +18,7 @@ import { Schemas } from '../../../../base/common/network.js'; import { resolveImageEditorAttachContext } from './chatAttachmentResolve.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { equals } from '../../../../base/common/objects.js'; +import { Iterable } from '../../../../base/common/iterator.js'; export interface IChatAttachmentChangeEvent { readonly deleted: readonly string[]; @@ -63,31 +64,6 @@ export class ChatAttachmentModel extends Disposable { return new Set(this._attachments.keys()); } - clear(clearStickyAttachments: boolean = false): void { - const deleted = Array.from(this._attachments.keys()); - this._attachments.clear(); - - if (clearStickyAttachments) { - this.promptInstructions.clear(); - } - - this._onDidChange.fire({ deleted, added: [], updated: [] }); - } - - delete(...variableEntryIds: string[]) { - const deleted: string[] = []; - - for (const variableEntryId of variableEntryIds) { - if (this._attachments.delete(variableEntryId)) { - deleted.push(variableEntryId); - } - } - - if (deleted.length > 0) { - this._onDidChange.fire({ deleted, added: [], updated: [] }); - } - } - async addFile(uri: URI, range?: IRange) { if (/\.(png|jpe?g|gif|bmp|webp)$/i.test(uri.path)) { const context = await this.asImageVariableEntry(uri); @@ -95,9 +71,9 @@ export class ChatAttachmentModel extends Disposable { this.addContext(context); } return; + } else { + this.addContext(this.asFileVariableEntry(uri, range)); } - - this.addContext(this.asVariableEntry(uri, range)); } addFolder(uri: URI) { @@ -109,7 +85,68 @@ export class ChatAttachmentModel extends Disposable { }); } - asVariableEntry(uri: URI, range?: IRange): IChatRequestVariableEntry { + clear(clearStickyAttachments: boolean = false): void { + const deleted = Array.from(this._attachments.keys()); + this._attachments.clear(); + + if (clearStickyAttachments) { + this.promptInstructions.clear(); + } + + this._onDidChange.fire({ deleted, added: [], updated: [] }); + } + + addContext(...attachments: IChatRequestVariableEntry[]) { + attachments = attachments.filter(attachment => !this._attachments.has(attachment.id)); + this.updateContent(Iterable.empty(), attachments); + } + + clearAndSetContext(...attachments: IChatRequestVariableEntry[]) { + this.updateContent(Array.from(this._attachments.keys()), attachments); + } + + delete(...variableEntryIds: string[]) { + this.updateContent(variableEntryIds, Iterable.empty()); + } + + updateContent(toDelete: Iterable, upsert: Iterable) { + const deleted: string[] = []; + const added: IChatRequestVariableEntry[] = []; + const updated: IChatRequestVariableEntry[] = []; + + for (const id of toDelete) { + if (this._attachments.delete(id)) { + deleted.push(id); + } + } + + for (const item of upsert) { + + if (item.kind === 'promptFile') { + // TODO@jrieken @aeschli @legomushroom Let's make instructions normal + // attachment types so that this isn't needed + this.promptInstructions.add(item.value as URI); + continue; + } + + const oldItem = this._attachments.get(item.id); + if (!oldItem) { + this._attachments.set(item.id, item); + added.push(item); + } else if (!equals(oldItem, item)) { + this._attachments.set(item.id, item); + updated.push(item); + } + } + + if (deleted.length > 0 || added.length > 0 || updated.length > 0) { + this._onDidChange.fire({ deleted, added, updated }); + } + } + + // ---- create utils + + asFileVariableEntry(uri: URI, range?: IRange): IChatRequestFileEntry { return { kind: 'file', value: range ? { uri, range } : uri, @@ -132,60 +169,4 @@ export class ChatAttachmentModel extends Disposable { return undefined; } - addContext(...attachments: IChatRequestVariableEntry[]) { - const added: IChatRequestVariableEntry[] = []; - - for (const attachment of attachments) { - if (!this._attachments.has(attachment.id)) { - this._attachments.set(attachment.id, attachment); - added.push(attachment); - } - } - - if (added.length > 0) { - this._onDidChange.fire({ deleted: [], added, updated: [] }); - } - } - - clearAndSetContext(...attachments: IChatRequestVariableEntry[]) { - const deleted = Array.from(this._attachments.keys()); - this._attachments.clear(); - - const added: IChatRequestVariableEntry[] = []; - for (const attachment of attachments) { - this._attachments.set(attachment.id, attachment); - added.push(attachment); - } - - if (deleted.length > 0 || added.length > 0) { - this._onDidChange.fire({ deleted, added, updated: [] }); - } - } - - updateContent(toDelete: Iterable, upsert: Iterable) { - const deleted: string[] = []; - const added: IChatRequestVariableEntry[] = []; - const updated: IChatRequestVariableEntry[] = []; - - for (const id of toDelete) { - if (this._attachments.delete(id)) { - deleted.push(id); - } - } - - for (const item of upsert) { - const oldItem = this._attachments.get(item.id); - if (!oldItem) { - this._attachments.set(item.id, item); - added.push(item); - } else if (!equals(oldItem, item)) { - this._attachments.set(item.id, item); - updated.push(item); - } - } - - if (deleted.length > 0 || added.length > 0 || updated.length > 0) { - this._onDidChange.fire({ deleted, added, updated }); - } - } } diff --git a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatPromptAttachmentModel.ts b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatPromptAttachmentModel.ts index 55f85ce45d7..c56f404de0f 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatPromptAttachmentModel.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatPromptAttachmentModel.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { URI } from '../../../../../base/common/uri.js'; +import { pick } from '../../../../../base/common/arrays.js'; import { Emitter } from '../../../../../base/common/event.js'; -import { IDisposable } from '../../../../../base/common/lifecycle.js'; import { PromptParser } from '../../common/promptSyntax/parsers/promptParser.js'; import { BasePromptParser } from '../../common/promptSyntax/parsers/basePromptParser.js'; import { ObservableDisposable } from '../../../../../base/common/observableDisposable.js'; @@ -51,7 +51,7 @@ export class ChatPromptAttachmentModel extends ObservableDisposable { // otherwise return `URI` for the main reference and // all valid child `URI` references it may contain return [ - ...reference.allValidReferencesUris, + ...reference.allValidReferences.map(pick('uri')), reference.uri, ]; } @@ -93,21 +93,20 @@ export class ChatPromptAttachmentModel extends ObservableDisposable { */ protected _onUpdate = this._register(new Emitter()); /** - * Subscribe to the `onUpdate` event. - * @param callback Function to invoke on update. + * Subscribe to the event that fires when the underlying prompt + * reference instance is updated. + * See {@link BasePromptParser.onUpdate}. */ - public onUpdate(callback: () => unknown): IDisposable { - return this._onUpdate.event(callback); - } + public readonly onUpdate = this._onUpdate.event; constructor( public readonly uri: URI, - @IInstantiationService private readonly initService: IInstantiationService, + @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); this._reference = this._register( - this.initService.createInstance( + this.instantiationService.createInstance( PromptParser, this.uri, // in this case we know that the attached file must have been a diff --git a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatPromptAttachmentsCollection.ts b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatPromptAttachmentsCollection.ts index 7f6445866ab..2e6e245c886 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatPromptAttachmentsCollection.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatPromptAttachmentsCollection.ts @@ -224,12 +224,10 @@ export class ChatPromptAttachmentsCollection extends Disposable { } constructor( - @IInstantiationService private readonly initService: IInstantiationService, + @IInstantiationService private readonly instantiationService: IInstantiationService, @IConfigurationService private readonly configService: IConfigurationService, ) { super(); - - this._onUpdate.fire = this._onUpdate.fire.bind(this._onUpdate); } /** @@ -250,8 +248,8 @@ export class ChatPromptAttachmentsCollection extends Disposable { continue; } - const instruction = this.initService.createInstance(ChatPromptAttachmentModel, uri); - instruction.addDisposable( + const instruction = this.instantiationService.createInstance(ChatPromptAttachmentModel, uri); + instruction.addDisposables( instruction.onDispose(() => { // note! we have to use `deleteAndLeak` here, because the `*AndDispose` // alternative results in an infinite loop of calling this callback diff --git a/src/vs/workbench/contrib/chat/browser/chatAttachmentWidgets.ts b/src/vs/workbench/contrib/chat/browser/chatAttachmentWidgets.ts index 356cbf7f7c0..e1dafd98d1f 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAttachmentWidgets.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAttachmentWidgets.ts @@ -325,7 +325,7 @@ function createImageElements(resource: URI | undefined, name: string, fullName: disposable.add(hoverService.setupDelayedHover(element, { content: hoverElement, appearance: { showPointer: true } })); - const blob = new Blob([buffer], { type: 'image/png' }); + const blob = new Blob([buffer as Uint8Array], { type: 'image/png' }); const url = URL.createObjectURL(blob); const pillImg = dom.$('img.chat-attached-context-pill-image', { src: url, alt: '' }); const pill = dom.$('div.chat-attached-context-pill', {}, pillImg); diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts index 6414bc84374..565a0df6331 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts @@ -25,7 +25,7 @@ export class ChatAttachmentsContentPart extends Disposable { private readonly attachedContextDisposables = this._register(new DisposableStore()); private readonly _onDidChangeVisibility = this._register(new Emitter()); - private readonly _contextResourceLabels = this._register(this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this._onDidChangeVisibility.event })); + private readonly _contextResourceLabels: ResourceLabels; constructor( private readonly variables: IChatRequestVariableEntry[], @@ -34,6 +34,7 @@ export class ChatAttachmentsContentPart extends Disposable { @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); + this._contextResourceLabels = this._register(this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this._onDidChangeVisibility.event })); this.initAttachedContext(domNode); if (!domNode.childElementCount) { diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationWidget.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationWidget.ts index f23f60ebe20..46b09570ba4 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationWidget.ts @@ -13,6 +13,7 @@ import { IMarkdownRenderResult, MarkdownRenderer, openLinkFromMarkdown } from '. import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { FocusMode } from '../../../../../platform/native/common/native.js'; import { IOpenerService } from '../../../../../platform/opener/common/opener.js'; import { defaultButtonStyles } from '../../../../../platform/theme/browser/defaultStyles.js'; import { IHostService } from '../../../../services/host/browser/host.js'; @@ -173,7 +174,7 @@ abstract class BaseChatConfirmationWidget extends Disposable { if (this._configurationService.getValue('chat.focusWindowOnConfirmation')) { const targetWindow = dom.getWindow(listContainer); if (!targetWindow.document.hasFocus()) { - this._hostService.focus(targetWindow, { force: true /* Application may not be active */ }); + this._hostService.focus(targetWindow, { mode: FocusMode.Notify }); } } } diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatProgressContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatProgressContentPart.ts index 6c5c13e045e..0da8909b529 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatProgressContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatProgressContentPart.ts @@ -9,14 +9,13 @@ import { Codicon } from '../../../../../base/common/codicons.js'; import { MarkdownString } from '../../../../../base/common/htmlContent.js'; import { Disposable } from '../../../../../base/common/lifecycle.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; -import { URI } from '../../../../../base/common/uri.js'; import { MarkdownRenderer } from '../../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js'; import { localize } from '../../../../../nls.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; -import { IChatProgressMessage, IChatTask } from '../../common/chatService.js'; +import { IChatProgressMessage, IChatTask, IChatTaskSerialized } from '../../common/chatService.js'; import { IChatRendererContent, IChatWorkingProgress, isResponseVM } from '../../common/chatViewModel.js'; import { ChatTreeItem } from '../chat.js'; -import { InlineAnchorWidget } from '../chatInlineAnchorWidget.js'; +import { renderFileWidgets } from '../chatInlineAnchorWidget.js'; import { IChatContentPart, IChatContentPartRenderContext } from './chatContentParts.js'; import { IChatMarkdownAnchorService } from './chatMarkdownAnchorService.js'; @@ -27,7 +26,7 @@ export class ChatProgressContentPart extends Disposable implements IChatContentP private readonly isHidden: boolean; constructor( - progress: IChatProgressMessage | IChatTask, + progress: IChatProgressMessage | IChatTask | IChatTaskSerialized, renderer: MarkdownRenderer, context: IChatContentPartRenderContext, forceShowSpinner: boolean | undefined, @@ -55,7 +54,7 @@ export class ChatProgressContentPart extends Disposable implements IChatContentP const codicon = icon ? icon : this.showSpinner ? ThemeIcon.modify(Codicon.loading, 'spin') : Codicon.check; const result = this._register(renderer.render(progress.content)); result.element.classList.add('progress-step'); - this.renderFileWidgets(result.element); + renderFileWidgets(result.element, this.instantiationService, this.chatMarkdownAnchorService, this._store); this.domNode = $('.progress-container'); const iconElement = $('div'); @@ -64,21 +63,6 @@ export class ChatProgressContentPart extends Disposable implements IChatContentP append(this.domNode, result.element); } - private renderFileWidgets(element: HTMLElement): void { - const links = element.querySelectorAll('a'); - links.forEach(a => { - // Empty link text -> render file widget - if (!a.textContent?.trim()) { - const href = a.getAttribute('data-href'); - const uri = href ? URI.parse(href) : undefined; - if (uri?.scheme) { - const widget = this._register(this.instantiationService.createInstance(InlineAnchorWidget, a, { kind: 'inlineReference', inlineReference: uri })); - this._register(this.chatMarkdownAnchorService.register(widget)); - } - } - }); - } - hasSameContent(other: IChatRendererContent, followingContent: IChatRendererContent[], element: ChatTreeItem): boolean { // Progress parts render render until some other content shows up, then they hide. // When some other content shows up, need to signal to be rerendered as hidden. diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts index 291f81a0c8c..d3dafb6d926 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts @@ -15,6 +15,7 @@ import { basename } from '../../../../../base/common/path.js'; import { basenameOrAuthority, isEqualAuthority } from '../../../../../base/common/resources.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { URI } from '../../../../../base/common/uri.js'; +import { IRange } from '../../../../../editor/common/core/range.js'; import { localize, localize2 } from '../../../../../nls.js'; import { getFlatContextMenuActions } from '../../../../../platform/actions/browser/menuEntryActionViewItem.js'; import { MenuWorkbenchToolBar } from '../../../../../platform/actions/browser/toolbar.js'; @@ -31,7 +32,7 @@ import { IOpenerService } from '../../../../../platform/opener/common/opener.js' import { IProductService } from '../../../../../platform/product/common/productService.js'; import { IThemeService } from '../../../../../platform/theme/common/themeService.js'; import { fillEditorsDragData } from '../../../../browser/dnd.js'; -import { IResourceLabel, ResourceLabels } from '../../../../browser/labels.js'; +import { IResourceLabel, IResourceLabelProps, ResourceLabels } from '../../../../browser/labels.js'; import { ColorScheme } from '../../../../browser/web.api.js'; import { ResourceContextKey } from '../../../../common/contextkeys.js'; import { SETTINGS_AUTHORITY } from '../../../../services/preferences/common/preferences.js'; @@ -366,9 +367,7 @@ class CollapsibleListRenderer implements IListRenderer()); - public readonly onDidChangeHeight = this._onDidChangeHeight.event; - - public get codeblocks(): IChatCodeBlockInfo[] { - return this.subPart?.codeblocks ?? []; - } - - public get codeblocksPartId(): string | undefined { - return this.subPart?.codeblocksPartId; - } - - private subPart!: ChatToolInvocationSubPart; - - constructor( - private readonly toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized, - context: IChatContentPartRenderContext, - renderer: MarkdownRenderer, - listPool: CollapsibleListPool, - editorPool: EditorPool, - currentWidthDelegate: () => number, - codeBlockModelCollection: CodeBlockModelCollection, - codeBlockStartIndex: number, - @IInstantiationService instantiationService: IInstantiationService, - ) { - super(); - - this.domNode = dom.$('.chat-tool-invocation-part'); - if (toolInvocation.presentation === 'hidden') { - return; - } - - // This part is a bit different, since IChatToolInvocation is not an immutable model object. So this part is able to rerender itself. - // If this turns out to be a typical pattern, we could come up with a more reusable pattern, like telling the list to rerender an element - // when the model changes, or trying to make the model immutable and swap out one content part for a new one based on user actions in the view. - const partStore = this._register(new DisposableStore()); - const render = () => { - dom.clearNode(this.domNode); - partStore.clear(); - - this.subPart = partStore.add(instantiationService.createInstance(ChatToolInvocationSubPart, toolInvocation, context, renderer, listPool, editorPool, currentWidthDelegate, codeBlockModelCollection, codeBlockStartIndex)); - this.domNode.appendChild(this.subPart.domNode); - partStore.add(this.subPart.onDidChangeHeight(() => this._onDidChangeHeight.fire())); - partStore.add(this.subPart.onNeedsRerender(() => { - render(); - this._onDidChangeHeight.fire(); - })); - }; - render(); - } - - hasSameContent(other: IChatRendererContent, followingContent: IChatRendererContent[], element: ChatTreeItem): boolean { - return (other.kind === 'toolInvocation' || other.kind === 'toolInvocationSerialized') && this.toolInvocation.toolCallId === other.toolCallId; - } - - addDisposable(disposable: IDisposable): void { - this._register(disposable); - } -} - -class ChatToolInvocationSubPart extends Disposable { - private static idPool = 0; - /** Remembers expanded tool parts on re-render */ - private static readonly _expandedByDefault = new WeakMap(); - - private readonly _codeblocksPartId = 'tool-' + (ChatToolInvocationSubPart.idPool++); - - public readonly domNode: HTMLElement; - - private _onNeedsRerender = this._register(new Emitter()); - public readonly onNeedsRerender = this._onNeedsRerender.event; - - private _onDidChangeHeight = this._register(new Emitter()); - public readonly onDidChangeHeight = this._onDidChangeHeight.event; - - private markdownPart: ChatMarkdownContentPart | undefined; - private _codeblocks: IChatCodeBlockInfo[] = []; - public get codeblocks(): IChatCodeBlockInfo[] { - // TODO this is weird, the separate cases should maybe be their own "subparts" - return this.markdownPart?.codeblocks ?? this._codeblocks; - } - - public get codeblocksPartId(): string { - return this.markdownPart?.codeblocksPartId ?? this._codeblocksPartId; - } - - constructor( - private readonly toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized, - private readonly context: IChatContentPartRenderContext, - private readonly renderer: MarkdownRenderer, - private readonly listPool: CollapsibleListPool, - private readonly editorPool: EditorPool, - private readonly currentWidthDelegate: () => number, - private readonly codeBlockModelCollection: CodeBlockModelCollection, - private readonly codeBlockStartIndex: number, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @IKeybindingService private readonly keybindingService: IKeybindingService, - @IModelService private readonly modelService: IModelService, - @ILanguageService private readonly languageService: ILanguageService, - @IContextKeyService private readonly contextKeyService: IContextKeyService, - @ILanguageModelToolsService private readonly languageModelToolsService: ILanguageModelToolsService, - @ICommandService private readonly commandService: ICommandService, - @IMarkerService private readonly markerService: IMarkerService, - @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, - ) { - super(); - - if (toolInvocation.kind === 'toolInvocation' && toolInvocation.confirmationMessages) { - if (toolInvocation.toolSpecificData?.kind === 'terminal') { - this.domNode = this.createTerminalConfirmationWidget(toolInvocation, toolInvocation.toolSpecificData); - } else { - this.domNode = this.createConfirmationWidget(toolInvocation); - } - } else if (toolInvocation.toolSpecificData?.kind === 'terminal') { - this.domNode = this.createTerminalMarkdownProgressPart(toolInvocation, toolInvocation.toolSpecificData); - } else if (Array.isArray(toolInvocation.resultDetails) && toolInvocation.resultDetails?.length) { - this.domNode = this.createResultList(toolInvocation.pastTenseMessage ?? toolInvocation.invocationMessage, toolInvocation.resultDetails); - } else if (isToolResultInputOutputDetails(toolInvocation.resultDetails)) { - this.domNode = this.createInputOutputMarkdownProgressPart(toolInvocation.pastTenseMessage ?? toolInvocation.invocationMessage, toolInvocation.originMessage, toolInvocation.resultDetails.input, toolInvocation.resultDetails.output, !!toolInvocation.resultDetails.isError); - } else if (toolInvocation.kind === 'toolInvocation' && toolInvocation.toolSpecificData?.kind === 'input' && !toolInvocation.isComplete) { - this.domNode = this.createInputOutputMarkdownProgressPart(this.toolInvocation.invocationMessage, toolInvocation.originMessage, typeof toolInvocation.toolSpecificData.rawInput === 'string' ? toolInvocation.toolSpecificData.rawInput : JSON.stringify(toolInvocation.toolSpecificData.rawInput, null, 2), undefined, false); - } else { - this.domNode = this.createProgressPart(); - } - - if (toolInvocation.kind === 'toolInvocation' && !toolInvocation.isComplete) { - toolInvocation.isCompletePromise.then(() => this._onNeedsRerender.fire()); - } - } - - private createConfirmationWidget(toolInvocation: IChatToolInvocation): HTMLElement { - if (!toolInvocation.confirmationMessages) { - throw new Error('Confirmation messages are missing'); - } - const { title, message, allowAutoConfirm } = toolInvocation.confirmationMessages; - const continueLabel = localize('continue', "Continue"); - const continueKeybinding = this.keybindingService.lookupKeybinding(AcceptToolConfirmationActionId)?.getLabel(); - const continueTooltip = continueKeybinding ? `${continueLabel} (${continueKeybinding})` : continueLabel; - const cancelLabel = localize('cancel', "Cancel"); - const cancelKeybinding = this.keybindingService.lookupKeybinding(CancelChatActionId)?.getLabel(); - const cancelTooltip = cancelKeybinding ? `${cancelLabel} (${cancelKeybinding})` : cancelLabel; - - const enum ConfirmationOutcome { - Allow, - Disallow, - AllowWorkspace, - AllowGlobally, - AllowSession, - } - - const buttons: IChatConfirmationButton[] = [ - { - label: continueLabel, - data: ConfirmationOutcome.Allow, - tooltip: continueTooltip, - moreActions: !allowAutoConfirm ? undefined : [ - { label: localize('allowSession', 'Allow in this Session'), data: ConfirmationOutcome.AllowSession, tooltip: localize('allowSesssionTooltip', 'Allow this tool to run in this session without confirmation.') }, - { label: localize('allowWorkspace', 'Allow in this Workspace'), data: ConfirmationOutcome.AllowWorkspace, tooltip: localize('allowWorkspaceTooltip', 'Allow this tool to run in this workspace without confirmation.') }, - { label: localize('allowGlobally', 'Always Allow'), data: ConfirmationOutcome.AllowGlobally, tooltip: localize('allowGloballTooltip', 'Always allow this tool to run without confirmation.') }, - ], - }, - { - label: localize('cancel', "Cancel"), - data: ConfirmationOutcome.Disallow, - isSecondary: true, - tooltip: cancelTooltip - }]; - let confirmWidget: ChatConfirmationWidget | ChatCustomConfirmationWidget; - if (typeof message === 'string') { - confirmWidget = this._register(this.instantiationService.createInstance( - ChatConfirmationWidget, - title, - toolInvocation.originMessage, - message, - buttons, - this.context.container, - )); - } else { - const chatMarkdownContent: IChatMarkdownContent = { - kind: 'markdownContent', - content: message, - }; - const codeBlockRenderOptions: ICodeBlockRenderOptions = { - hideToolbar: true, - reserveWidth: 19, - verticalPadding: 5, - editorOptions: { - wordWrap: 'on' - } - }; - - const elements = dom.h('div', [ - dom.h('.message@message'), - dom.h('.editor@editor'), - ]); - - if (toolInvocation.toolSpecificData?.kind === 'input' && toolInvocation.toolSpecificData.rawInput && !isEmptyObject(toolInvocation.toolSpecificData.rawInput)) { - - const inputData = toolInvocation.toolSpecificData; - - const codeBlockRenderOptions: ICodeBlockRenderOptions = { - hideToolbar: true, - reserveWidth: 19, - maxHeightInLines: 13, - verticalPadding: 5, - editorOptions: { - wordWrap: 'off', - readOnly: false - } - }; - - const langId = this.languageService.getLanguageIdByLanguageName('json'); - const rawJsonInput = JSON.stringify(inputData.rawInput ?? {}, null, 1); - const canSeeMore = count(rawJsonInput, '\n') > 2; // if more than one key:value - const model = this._register(this.modelService.createModel( - // View a single JSON line by default until they 'see more' - rawJsonInput.replace(/\n */g, ' '), - this.languageService.createById(langId), - createToolInputUri(toolInvocation.toolId) - )); - - const markerOwner = generateUuid(); - const schemaUri = createToolSchemaUri(toolInvocation.toolId); - const validator = new RunOnceScheduler(async () => { - - const newMarker: IMarkerData[] = []; - - const result = await this.commandService.executeCommand('json.validate', schemaUri, model.getValue()); - for (const item of result) { - if (item.range && item.message) { - newMarker.push({ - severity: item.severity === 'Error' ? MarkerSeverity.Error : MarkerSeverity.Warning, - message: item.message, - startLineNumber: item.range[0].line + 1, - startColumn: item.range[0].character + 1, - endLineNumber: item.range[1].line + 1, - endColumn: item.range[1].character + 1, - code: item.code ? String(item.code) : undefined - }); - } - } - - this.markerService.changeOne(markerOwner, model.uri, newMarker); - }, 500); - - validator.schedule(); - this._register(model.onDidChangeContent(() => validator.schedule())); - this._register(toDisposable(() => this.markerService.remove(markerOwner, [model.uri]))); - this._register(validator); - - const editor = this._register(this.editorPool.get()); - editor.object.render({ - codeBlockIndex: this.codeBlockStartIndex, - codeBlockPartIndex: 0, - element: this.context.element, - languageId: langId ?? 'json', - renderOptions: codeBlockRenderOptions, - textModel: Promise.resolve(model), - chatSessionId: this.context.element.sessionId - }, this.currentWidthDelegate()); - this._codeblocks.push({ - codeBlockIndex: this.codeBlockStartIndex, - codemapperUri: undefined, - elementId: this.context.element.id, - focus: () => editor.object.focus(), - isStreaming: false, - ownerMarkdownPartId: this.codeblocksPartId, - uri: model.uri, - uriPromise: Promise.resolve(model.uri), - chatSessionId: this.context.element.sessionId - }); - this._register(editor.object.onDidChangeContentHeight(() => { - editor.object.layout(this.currentWidthDelegate()); - this._onDidChangeHeight.fire(); - })); - this._register(model.onDidChangeContent(e => { - try { - inputData.rawInput = JSON.parse(model.getValue()); - } catch { - // ignore - } - })); - - elements.editor.append(editor.object.element); - - if (canSeeMore) { - const seeMore = dom.h('div.see-more', [dom.h('a@link')]); - seeMore.link.textContent = localize('seeMore', "See more"); - this._register(dom.addDisposableGenericMouseDownListener(seeMore.link, () => { - try { - const parsed = JSON.parse(model.getValue()); - model.setValue(JSON.stringify(parsed, null, 2)); - editor.object.editor.updateOptions({ wordWrap: 'on' }); - } catch { - // ignored - } - seeMore.root.remove(); - })); - elements.editor.append(seeMore.root); - } - } - - this.markdownPart = this._register(this.instantiationService.createInstance(ChatMarkdownContentPart, chatMarkdownContent, this.context, this.editorPool, false, this.codeBlockStartIndex, this.renderer, this.currentWidthDelegate(), this.codeBlockModelCollection, { codeBlockRenderOptions })); - elements.message.append(this.markdownPart.domNode); - - this._register(this.markdownPart.onDidChangeHeight(() => this._onDidChangeHeight.fire())); - confirmWidget = this._register(this.instantiationService.createInstance( - ChatCustomConfirmationWidget, - title, - toolInvocation.originMessage, - elements.root, - buttons, - this.context.container, - )); - } - - const hasToolConfirmation = ChatContextKeys.Editing.hasToolConfirmation.bindTo(this.contextKeyService); - hasToolConfirmation.set(true); - - this._register(confirmWidget.onDidClick(button => { - switch (button.data as ConfirmationOutcome) { - case ConfirmationOutcome.AllowGlobally: - this.languageModelToolsService.setToolAutoConfirmation(toolInvocation.toolId, 'profile', true); - toolInvocation.confirmed.complete(true); - break; - case ConfirmationOutcome.AllowWorkspace: - this.languageModelToolsService.setToolAutoConfirmation(toolInvocation.toolId, 'workspace', true); - toolInvocation.confirmed.complete(true); - break; - case ConfirmationOutcome.AllowSession: - this.languageModelToolsService.setToolAutoConfirmation(toolInvocation.toolId, 'memory', true); - toolInvocation.confirmed.complete(true); - break; - case ConfirmationOutcome.Allow: - toolInvocation.confirmed.complete(true); - break; - case ConfirmationOutcome.Disallow: - toolInvocation.confirmed.complete(false); - break; - } - - this.chatWidgetService.getWidgetBySessionId(this.context.element.sessionId)?.focusInput(); - })); - this._register(confirmWidget.onDidChangeHeight(() => this._onDidChangeHeight.fire())); - this._register(toDisposable(() => hasToolConfirmation.reset())); - toolInvocation.confirmed.p.then(() => { - hasToolConfirmation.reset(); - this._onNeedsRerender.fire(); - }); - return confirmWidget.domNode; - } - - private createTerminalConfirmationWidget(toolInvocation: IChatToolInvocation, terminalData: IChatTerminalToolInvocationData): HTMLElement { - if (!toolInvocation.confirmationMessages) { - throw new Error('Confirmation messages are missing'); - } - const title = toolInvocation.confirmationMessages.title; - const message = toolInvocation.confirmationMessages.message; - const continueLabel = localize('continue', "Continue"); - const continueKeybinding = this.keybindingService.lookupKeybinding(AcceptToolConfirmationActionId)?.getLabel(); - const continueTooltip = continueKeybinding ? `${continueLabel} (${continueKeybinding})` : continueLabel; - const cancelLabel = localize('cancel', "Cancel"); - const cancelKeybinding = this.keybindingService.lookupKeybinding(CancelChatActionId)?.getLabel(); - const cancelTooltip = cancelKeybinding ? `${cancelLabel} (${cancelKeybinding})` : cancelLabel; - - const buttons: IChatConfirmationButton[] = [ - { - label: continueLabel, - data: true, - tooltip: continueTooltip - }, - { - label: cancelLabel, - data: false, - isSecondary: true, - tooltip: cancelTooltip - }]; - const renderedMessage = this._register(this.renderer.render( - typeof message === 'string' ? new MarkdownString(message) : message, - { asyncRenderCallback: () => this._onDidChangeHeight.fire() } - )); - const codeBlockRenderOptions: ICodeBlockRenderOptions = { - hideToolbar: true, - reserveWidth: 19, - verticalPadding: 5, - editorOptions: { - wordWrap: 'on', - readOnly: false - } - }; - const langId = this.languageService.getLanguageIdByLanguageName(terminalData.language ?? 'sh') ?? 'shellscript'; - const model = this.modelService.createModel(terminalData.command, this.languageService.createById(langId)); - const editor = this._register(this.editorPool.get()); - const renderPromise = editor.object.render({ - codeBlockIndex: this.codeBlockStartIndex, - codeBlockPartIndex: 0, - element: this.context.element, - languageId: langId, - renderOptions: codeBlockRenderOptions, - textModel: Promise.resolve(model), - chatSessionId: this.context.element.sessionId - }, this.currentWidthDelegate()); - this._register(thenIfNotDisposed(renderPromise, () => this._onDidChangeHeight.fire())); - this._codeblocks.push({ - codeBlockIndex: this.codeBlockStartIndex, - codemapperUri: undefined, - elementId: this.context.element.id, - focus: () => editor.object.focus(), - isStreaming: false, - ownerMarkdownPartId: this.codeblocksPartId, - uri: model.uri, - uriPromise: Promise.resolve(model.uri), - chatSessionId: this.context.element.sessionId - }); - this._register(editor.object.onDidChangeContentHeight(() => { - editor.object.layout(this.currentWidthDelegate()); - this._onDidChangeHeight.fire(); - })); - this._register(model.onDidChangeContent(e => { - terminalData.command = model.getValue(); - })); - const element = dom.$(''); - dom.append(element, editor.object.element); - dom.append(element, renderedMessage.element); - const confirmWidget = this._register(this.instantiationService.createInstance( - ChatCustomConfirmationWidget, - title, - undefined, - element, - buttons, - this.context.container, - )); - - ChatContextKeys.Editing.hasToolConfirmation.bindTo(this.contextKeyService).set(true); - this._register(confirmWidget.onDidClick(button => { - toolInvocation.confirmed.complete(button.data); - this.chatWidgetService.getWidgetBySessionId(this.context.element.sessionId)?.focusInput(); - })); - this._register(confirmWidget.onDidChangeHeight(() => this._onDidChangeHeight.fire())); - toolInvocation.confirmed.p.then(() => { - ChatContextKeys.Editing.hasToolConfirmation.bindTo(this.contextKeyService).set(false); - this._onNeedsRerender.fire(); - }); - return confirmWidget.domNode; - } - - private createProgressPart(): HTMLElement { - if (this.toolInvocation.isComplete && this.toolInvocation.isConfirmed !== false && this.toolInvocation.pastTenseMessage) { - const part = this.renderProgressContent(this.toolInvocation.pastTenseMessage); - this._register(part); - return part.domNode; - } else { - const container = document.createElement('div'); - const progressObservable = this.toolInvocation.kind === 'toolInvocation' ? this.toolInvocation.progress : undefined; - this._register(autorunWithStore((reader, store) => { - const progress = progressObservable?.read(reader); - const part = store.add(this.renderProgressContent(progress?.message || this.toolInvocation.invocationMessage)); - dom.reset(container, part.domNode); - })); - return container; - } - } - - private renderProgressContent(content: IMarkdownString | string) { - if (typeof content === 'string') { - content = new MarkdownString().appendText(content); - } - - const progressMessage: IChatProgressMessage = { - kind: 'progressMessage', - content - }; - - const iconOverride = !this.toolInvocation.isConfirmed ? - Codicon.error : - this.toolInvocation.isComplete ? - Codicon.check : undefined; - return this.instantiationService.createInstance(ChatProgressContentPart, progressMessage, this.renderer, this.context, undefined, true, iconOverride); - } - - private createTerminalMarkdownProgressPart(toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized, terminalData: IChatTerminalToolInvocationData): HTMLElement { - const content = new MarkdownString(`\`\`\`${terminalData.language}\n${terminalData.command}\n\`\`\``); - const chatMarkdownContent: IChatMarkdownContent = { - kind: 'markdownContent', - content: content as IMarkdownString, - }; - - const codeBlockRenderOptions: ICodeBlockRenderOptions = { - hideToolbar: true, - reserveWidth: 19, - verticalPadding: 5, - editorOptions: { - wordWrap: 'on' - } - }; - this.markdownPart = this._register(this.instantiationService.createInstance(ChatMarkdownContentPart, chatMarkdownContent, this.context, this.editorPool, false, this.codeBlockStartIndex, this.renderer, this.currentWidthDelegate(), this.codeBlockModelCollection, { codeBlockRenderOptions })); - this._register(this.markdownPart.onDidChangeHeight(() => this._onDidChangeHeight.fire())); - const icon = !this.toolInvocation.isConfirmed ? - Codicon.error : - this.toolInvocation.isComplete ? - Codicon.check : ThemeIcon.modify(Codicon.loading, 'spin'); - const progressPart = this.instantiationService.createInstance(ChatCustomProgressPart, this.markdownPart.domNode, icon); - return progressPart.domNode; - } - - private createInputOutputMarkdownProgressPart(message: string | IMarkdownString, subtitle: string | IMarkdownString | undefined, input: string, output: IToolResultInputOutputDetails['output'] | undefined, isError: boolean): HTMLElement { - let codeBlockIndex = this.codeBlockStartIndex; - const toCodePart = (data: string): IChatCollapsibleIOCodePart => { - const model = this._register(this.modelService.createModel( - data, - this.languageService.createById('json') - )); - - return { - kind: 'code', - textModel: model, - languageId: model.getLanguageId(), - options: { - hideToolbar: true, - reserveWidth: 19, - maxHeightInLines: 13, - verticalPadding: 5, - editorOptions: { - wordWrap: 'on' - } - }, - codeBlockInfo: { - codeBlockIndex: codeBlockIndex++, - codemapperUri: undefined, - elementId: this.context.element.id, - focus: () => { }, - isStreaming: false, - ownerMarkdownPartId: this.codeblocksPartId, - uri: model.uri, - chatSessionId: this.context.element.sessionId, - uriPromise: Promise.resolve(model.uri) - } - }; - }; - - if (typeof output === 'string') { // back compat with older stored versions - output = [{ type: 'text', value: output }]; - } - - const collapsibleListPart = this._register(this.instantiationService.createInstance( - ChatCollapsibleInputOutputContentPart, - message, - subtitle, - this.context, - this.editorPool, - toCodePart(input), - output && { - parts: output.map((o): IChatCollapsibleIODataPart | IChatCollapsibleIOCodePart => { - if (o.type === 'data') { - const decoded = decodeBase64(o.value64).buffer; - if (getAttachableImageExtension(o.mimeType)) { - return { kind: 'data', value: decoded, mimeType: o.mimeType }; - } else { - return toCodePart(localize('toolResultData', "Data of type {0} ({1} bytes)", o.mimeType, decoded.byteLength)); - } - } else if (o.type === 'text') { - return toCodePart(o.value); - } else { - assertNever(o); - } - }), - }, - isError, - ChatToolInvocationSubPart._expandedByDefault.get(this.toolInvocation) ?? false, - )); - this._codeblocks.push(...collapsibleListPart.codeblocks); - this._register(collapsibleListPart.onDidChangeHeight(() => this._onDidChangeHeight.fire())); - this._register(toDisposable(() => ChatToolInvocationSubPart._expandedByDefault.set(this.toolInvocation, collapsibleListPart.expanded))); - - const progressObservable = this.toolInvocation.kind === 'toolInvocation' ? this.toolInvocation.progress : undefined; - if (progressObservable) { - this._register(autorunWithStore((reader, store) => { - const progress = progressObservable?.read(reader); - if (progress.message) { - collapsibleListPart.title = progress.message; - } - })); - } - - return collapsibleListPart.domNode; - } - - private createResultList( - message: string | IMarkdownString, - toolDetails: Array, - ): HTMLElement { - const collapsibleListPart = this._register(this.instantiationService.createInstance( - ChatCollapsibleListContentPart, - toolDetails.map(detail => ({ - kind: 'reference', - reference: detail, - })), - message, - this.context, - this.listPool, - )); - this._register(collapsibleListPart.onDidChangeHeight(() => this._onDidChangeHeight.fire())); - return collapsibleListPart.domNode; - } -} diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatInputOutputMarkdownProgressPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatInputOutputMarkdownProgressPart.ts new file mode 100644 index 00000000000..090ffae07fd --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatInputOutputMarkdownProgressPart.ts @@ -0,0 +1,134 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { assertNever } from '../../../../../../base/common/assert.js'; +import { decodeBase64 } from '../../../../../../base/common/buffer.js'; +import { IMarkdownString } from '../../../../../../base/common/htmlContent.js'; +import { toDisposable } from '../../../../../../base/common/lifecycle.js'; +import { autorun } from '../../../../../../base/common/observable.js'; +import { ILanguageService } from '../../../../../../editor/common/languages/language.js'; +import { IModelService } from '../../../../../../editor/common/services/model.js'; +import { localize } from '../../../../../../nls.js'; +import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; +import { IChatToolInvocation, IChatToolInvocationSerialized } from '../../../common/chatService.js'; +import { IToolResultInputOutputDetails } from '../../../common/languageModelToolsService.js'; +import { IChatCodeBlockInfo } from '../../chat.js'; +import { getAttachableImageExtension } from '../../chatAttachmentResolve.js'; +import { IChatContentPartRenderContext } from '../chatContentParts.js'; +import { EditorPool } from '../chatMarkdownContentPart.js'; +import { ChatCollapsibleInputOutputContentPart, IChatCollapsibleIOCodePart, IChatCollapsibleIODataPart } from '../chatToolInputOutputContentPart.js'; +import { BaseChatToolInvocationSubPart } from './chatToolInvocationSubPart.js'; + +export class ChatInputOutputMarkdownProgressPart extends BaseChatToolInvocationSubPart { + /** Remembers expanded tool parts on re-render */ + private static readonly _expandedByDefault = new WeakMap(); + + public readonly domNode: HTMLElement; + + private _codeblocks: IChatCodeBlockInfo[] = []; + public get codeblocks(): IChatCodeBlockInfo[] { + return this._codeblocks; + } + + constructor( + toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized, + context: IChatContentPartRenderContext, + editorPool: EditorPool, + codeBlockStartIndex: number, + message: string | IMarkdownString, + subtitle: string | IMarkdownString | undefined, + input: string, + output: IToolResultInputOutputDetails['output'] | undefined, + isError: boolean, + @IInstantiationService instantiationService: IInstantiationService, + @IModelService modelService: IModelService, + @ILanguageService languageService: ILanguageService, + ) { + super(toolInvocation); + + let codeBlockIndex = codeBlockStartIndex; + const toCodePart = (data: string): IChatCollapsibleIOCodePart => { + const model = this._register(modelService.createModel( + data, + languageService.createById('json'), + undefined, + true + )); + + return { + kind: 'code', + textModel: model, + languageId: model.getLanguageId(), + options: { + hideToolbar: true, + reserveWidth: 19, + maxHeightInLines: 13, + verticalPadding: 5, + editorOptions: { + wordWrap: 'on' + } + }, + codeBlockInfo: { + codeBlockIndex: codeBlockIndex++, + codemapperUri: undefined, + elementId: context.element.id, + focus: () => { }, + isStreaming: false, + ownerMarkdownPartId: this.codeblocksPartId, + uri: model.uri, + chatSessionId: context.element.sessionId, + uriPromise: Promise.resolve(model.uri) + } + }; + }; + + let processedOutput = output; + if (typeof output === 'string') { // back compat with older stored versions + processedOutput = [{ type: 'text', value: output }]; + } + + const collapsibleListPart = this._register(instantiationService.createInstance( + ChatCollapsibleInputOutputContentPart, + message, + subtitle, + context, + editorPool, + toCodePart(input), + processedOutput && { + parts: processedOutput.map((o): IChatCollapsibleIODataPart | IChatCollapsibleIOCodePart => { + if (o.type === 'data') { + const decoded = decodeBase64(o.value64).buffer; + if (getAttachableImageExtension(o.mimeType)) { + return { kind: 'data', value: decoded, mimeType: o.mimeType }; + } else { + return toCodePart(localize('toolResultData', "Data of type {0} ({1} bytes)", o.mimeType, decoded.byteLength)); + } + } else if (o.type === 'text') { + return toCodePart(o.value); + } else { + assertNever(o); + } + }), + }, + isError, + ChatInputOutputMarkdownProgressPart._expandedByDefault.get(toolInvocation) ?? false, + )); + this._codeblocks.push(...collapsibleListPart.codeblocks); + this._register(collapsibleListPart.onDidChangeHeight(() => this._onDidChangeHeight.fire())); + this._register(toDisposable(() => ChatInputOutputMarkdownProgressPart._expandedByDefault.set(toolInvocation, collapsibleListPart.expanded))); + + const progressObservable = toolInvocation.kind === 'toolInvocation' ? toolInvocation.progress : undefined; + if (progressObservable) { + this._register(autorun(reader => { + const progress = progressObservable?.read(reader); + if (progress.message) { + collapsibleListPart.title = progress.message; + } + })); + } + + this.domNode = collapsibleListPart.domNode; + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatResultListSubPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatResultListSubPart.ts new file mode 100644 index 00000000000..5df65ab89fd --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatResultListSubPart.ts @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IMarkdownString } from '../../../../../../base/common/htmlContent.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { Location } from '../../../../../../editor/common/languages.js'; +import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; +import { IChatToolInvocation, IChatToolInvocationSerialized } from '../../../common/chatService.js'; +import { IChatCodeBlockInfo } from '../../chat.js'; +import { IChatContentPartRenderContext } from '../chatContentParts.js'; +import { ChatCollapsibleListContentPart, CollapsibleListPool, IChatCollapsibleListItem } from '../chatReferencesContentPart.js'; +import { BaseChatToolInvocationSubPart } from './chatToolInvocationSubPart.js'; + +export class ChatResultListSubPart extends BaseChatToolInvocationSubPart { + public readonly domNode: HTMLElement; + public readonly codeblocks: IChatCodeBlockInfo[] = []; + + constructor( + toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized, + context: IChatContentPartRenderContext, + message: string | IMarkdownString, + toolDetails: Array, + listPool: CollapsibleListPool, + @IInstantiationService instantiationService: IInstantiationService, + ) { + super(toolInvocation); + + const collapsibleListPart = this._register(instantiationService.createInstance( + ChatCollapsibleListContentPart, + toolDetails.map(detail => ({ + kind: 'reference', + reference: detail, + })), + message, + context, + listPool, + )); + this._register(collapsibleListPart.onDidChangeHeight(() => this._onDidChangeHeight.fire())); + this.domNode = collapsibleListPart.domNode; + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatTerminalMarkdownProgressPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatTerminalMarkdownProgressPart.ts new file mode 100644 index 00000000000..d55090c7a38 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatTerminalMarkdownProgressPart.ts @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Codicon } from '../../../../../../base/common/codicons.js'; +import { MarkdownString } from '../../../../../../base/common/htmlContent.js'; +import { ThemeIcon } from '../../../../../../base/common/themables.js'; +import { MarkdownRenderer } from '../../../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js'; +import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; +import { IChatMarkdownContent, IChatTerminalToolInvocationData, IChatToolInvocation, IChatToolInvocationSerialized } from '../../../common/chatService.js'; +import { CodeBlockModelCollection } from '../../../common/codeBlockModelCollection.js'; +import { IChatCodeBlockInfo } from '../../chat.js'; +import { ICodeBlockRenderOptions } from '../../codeBlockPart.js'; +import { IChatContentPartRenderContext } from '../chatContentParts.js'; +import { ChatMarkdownContentPart, EditorPool } from '../chatMarkdownContentPart.js'; +import { ChatCustomProgressPart } from '../chatProgressContentPart.js'; +import { BaseChatToolInvocationSubPart } from './chatToolInvocationSubPart.js'; + +export class ChatTerminalMarkdownProgressPart extends BaseChatToolInvocationSubPart { + public readonly domNode: HTMLElement; + + private markdownPart: ChatMarkdownContentPart | undefined; + public get codeblocks(): IChatCodeBlockInfo[] { + return this.markdownPart?.codeblocks ?? []; + } + + constructor( + toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized, + terminalData: IChatTerminalToolInvocationData, + context: IChatContentPartRenderContext, + renderer: MarkdownRenderer, + editorPool: EditorPool, + currentWidthDelegate: () => number, + codeBlockStartIndex: number, + codeBlockModelCollection: CodeBlockModelCollection, + @IInstantiationService instantiationService: IInstantiationService, + ) { + super(toolInvocation); + + const content = new MarkdownString(`\`\`\`${terminalData.language}\n${terminalData.command}\n\`\`\``); + const chatMarkdownContent: IChatMarkdownContent = { + kind: 'markdownContent', + content: content, + }; + + const codeBlockRenderOptions: ICodeBlockRenderOptions = { + hideToolbar: true, + reserveWidth: 19, + verticalPadding: 5, + editorOptions: { + wordWrap: 'on' + } + }; + this.markdownPart = this._register(instantiationService.createInstance(ChatMarkdownContentPart, chatMarkdownContent, context, editorPool, false, codeBlockStartIndex, renderer, currentWidthDelegate(), codeBlockModelCollection, { codeBlockRenderOptions })); + this._register(this.markdownPart.onDidChangeHeight(() => this._onDidChangeHeight.fire())); + const icon = !toolInvocation.isConfirmed ? + Codicon.error : + toolInvocation.isComplete ? + Codicon.check : ThemeIcon.modify(Codicon.loading, 'spin'); + const progressPart = instantiationService.createInstance(ChatCustomProgressPart, this.markdownPart.domNode, icon); + this.domNode = progressPart.domNode; + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatTerminalToolSubPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatTerminalToolSubPart.ts new file mode 100644 index 00000000000..4911f9352ab --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatTerminalToolSubPart.ts @@ -0,0 +1,141 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from '../../../../../../base/browser/dom.js'; +import { MarkdownString } from '../../../../../../base/common/htmlContent.js'; +import { thenIfNotDisposed } from '../../../../../../base/common/lifecycle.js'; +import { MarkdownRenderer } from '../../../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js'; +import { ILanguageService } from '../../../../../../editor/common/languages/language.js'; +import { IModelService } from '../../../../../../editor/common/services/model.js'; +import { localize } from '../../../../../../nls.js'; +import { IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; +import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; +import { IKeybindingService } from '../../../../../../platform/keybinding/common/keybinding.js'; +import { ChatContextKeys } from '../../../common/chatContextKeys.js'; +import { IChatTerminalToolInvocationData, IChatToolInvocation } from '../../../common/chatService.js'; +import { CancelChatActionId } from '../../actions/chatExecuteActions.js'; +import { AcceptToolConfirmationActionId } from '../../actions/chatToolActions.js'; +import { IChatCodeBlockInfo, IChatWidgetService } from '../../chat.js'; +import { ICodeBlockRenderOptions } from '../../codeBlockPart.js'; +import { ChatCustomConfirmationWidget, IChatConfirmationButton } from '../chatConfirmationWidget.js'; +import { IChatContentPartRenderContext } from '../chatContentParts.js'; +import { EditorPool } from '../chatMarkdownContentPart.js'; +import { BaseChatToolInvocationSubPart } from './chatToolInvocationSubPart.js'; + +export class TerminalConfirmationWidgetSubPart extends BaseChatToolInvocationSubPart { + public readonly domNode: HTMLElement; + public readonly codeblocks: IChatCodeBlockInfo[] = []; + + constructor( + toolInvocation: IChatToolInvocation, + terminalData: IChatTerminalToolInvocationData, + private readonly context: IChatContentPartRenderContext, + private readonly renderer: MarkdownRenderer, + private readonly editorPool: EditorPool, + private readonly currentWidthDelegate: () => number, + private readonly codeBlockStartIndex: number, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IKeybindingService keybindingService: IKeybindingService, + @IModelService private readonly modelService: IModelService, + @ILanguageService private readonly languageService: ILanguageService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, + ) { + super(toolInvocation); + + if (!toolInvocation.confirmationMessages) { + throw new Error('Confirmation messages are missing'); + } + + const title = toolInvocation.confirmationMessages.title; + const message = toolInvocation.confirmationMessages.message; + const continueLabel = localize('continue', "Continue"); + const continueKeybinding = keybindingService.lookupKeybinding(AcceptToolConfirmationActionId)?.getLabel(); + const continueTooltip = continueKeybinding ? `${continueLabel} (${continueKeybinding})` : continueLabel; + const cancelLabel = localize('cancel', "Cancel"); + const cancelKeybinding = keybindingService.lookupKeybinding(CancelChatActionId)?.getLabel(); + const cancelTooltip = cancelKeybinding ? `${cancelLabel} (${cancelKeybinding})` : cancelLabel; + + const buttons: IChatConfirmationButton[] = [ + { + label: continueLabel, + data: true, + tooltip: continueTooltip + }, + { + label: cancelLabel, + data: false, + isSecondary: true, + tooltip: cancelTooltip + }]; + const renderedMessage = this._register(this.renderer.render( + typeof message === 'string' ? new MarkdownString(message) : message, + { asyncRenderCallback: () => this._onDidChangeHeight.fire() } + )); + const codeBlockRenderOptions: ICodeBlockRenderOptions = { + hideToolbar: true, + reserveWidth: 19, + verticalPadding: 5, + editorOptions: { + wordWrap: 'on', + readOnly: false + } + }; + const langId = this.languageService.getLanguageIdByLanguageName(terminalData.language ?? 'sh') ?? 'shellscript'; + const model = this.modelService.createModel(terminalData.command, this.languageService.createById(langId), undefined, true); + const editor = this._register(this.editorPool.get()); + const renderPromise = editor.object.render({ + codeBlockIndex: this.codeBlockStartIndex, + codeBlockPartIndex: 0, + element: this.context.element, + languageId: langId, + renderOptions: codeBlockRenderOptions, + textModel: Promise.resolve(model), + chatSessionId: this.context.element.sessionId + }, this.currentWidthDelegate()); + this._register(thenIfNotDisposed(renderPromise, () => this._onDidChangeHeight.fire())); + this.codeblocks.push({ + codeBlockIndex: this.codeBlockStartIndex, + codemapperUri: undefined, + elementId: this.context.element.id, + focus: () => editor.object.focus(), + isStreaming: false, + ownerMarkdownPartId: this.codeblocksPartId, + uri: model.uri, + uriPromise: Promise.resolve(model.uri), + chatSessionId: this.context.element.sessionId + }); + this._register(editor.object.onDidChangeContentHeight(() => { + editor.object.layout(this.currentWidthDelegate()); + this._onDidChangeHeight.fire(); + })); + this._register(model.onDidChangeContent(e => { + terminalData.command = model.getValue(); + })); + const element = dom.$(''); + dom.append(element, editor.object.element); + dom.append(element, renderedMessage.element); + const confirmWidget = this._register(this.instantiationService.createInstance( + ChatCustomConfirmationWidget, + title, + undefined, + element, + buttons, + this.context.container, + )); + + ChatContextKeys.Editing.hasToolConfirmation.bindTo(this.contextKeyService).set(true); + this._register(confirmWidget.onDidClick(button => { + toolInvocation.confirmed.complete(button.data); + this.chatWidgetService.getWidgetBySessionId(this.context.element.sessionId)?.focusInput(); + })); + this._register(confirmWidget.onDidChangeHeight(() => this._onDidChangeHeight.fire())); + toolInvocation.confirmed.p.then(() => { + ChatContextKeys.Editing.hasToolConfirmation.bindTo(this.contextKeyService).set(false); + this._onNeedsRerender.fire(); + }); + this.domNode = confirmWidget.domNode; + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatToolConfirmationSubPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatToolConfirmationSubPart.ts new file mode 100644 index 00000000000..e30bdcd147e --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatToolConfirmationSubPart.ts @@ -0,0 +1,287 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from '../../../../../../base/browser/dom.js'; +import { RunOnceScheduler } from '../../../../../../base/common/async.js'; +import { toDisposable } from '../../../../../../base/common/lifecycle.js'; +import { count } from '../../../../../../base/common/strings.js'; +import { isEmptyObject } from '../../../../../../base/common/types.js'; +import { generateUuid } from '../../../../../../base/common/uuid.js'; +import { MarkdownRenderer } from '../../../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js'; +import { ILanguageService } from '../../../../../../editor/common/languages/language.js'; +import { IModelService } from '../../../../../../editor/common/services/model.js'; +import { localize } from '../../../../../../nls.js'; +import { ICommandService } from '../../../../../../platform/commands/common/commands.js'; +import { IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; +import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; +import { IKeybindingService } from '../../../../../../platform/keybinding/common/keybinding.js'; +import { IMarkerData, IMarkerService, MarkerSeverity } from '../../../../../../platform/markers/common/markers.js'; +import { ChatContextKeys } from '../../../common/chatContextKeys.js'; +import { IChatMarkdownContent, IChatToolInvocation } from '../../../common/chatService.js'; +import { CodeBlockModelCollection } from '../../../common/codeBlockModelCollection.js'; +import { createToolInputUri, createToolSchemaUri, ILanguageModelToolsService } from '../../../common/languageModelToolsService.js'; +import { CancelChatActionId } from '../../actions/chatExecuteActions.js'; +import { AcceptToolConfirmationActionId } from '../../actions/chatToolActions.js'; +import { IChatCodeBlockInfo, IChatWidgetService } from '../../chat.js'; +import { renderFileWidgets } from '../../chatInlineAnchorWidget.js'; +import { ICodeBlockRenderOptions } from '../../codeBlockPart.js'; +import { ChatConfirmationWidget, ChatCustomConfirmationWidget, IChatConfirmationButton } from '../chatConfirmationWidget.js'; +import { IChatContentPartRenderContext } from '../chatContentParts.js'; +import { IChatMarkdownAnchorService } from '../chatMarkdownAnchorService.js'; +import { ChatMarkdownContentPart, EditorPool } from '../chatMarkdownContentPart.js'; +import { BaseChatToolInvocationSubPart } from './chatToolInvocationSubPart.js'; + +export class ToolConfirmationSubPart extends BaseChatToolInvocationSubPart { + public readonly domNode: HTMLElement; + + private markdownPart: ChatMarkdownContentPart | undefined; + public get codeblocks(): IChatCodeBlockInfo[] { + return this.markdownPart?.codeblocks ?? []; + } + + constructor( + toolInvocation: IChatToolInvocation, + private readonly context: IChatContentPartRenderContext, + private readonly renderer: MarkdownRenderer, + private readonly editorPool: EditorPool, + private readonly currentWidthDelegate: () => number, + private readonly codeBlockModelCollection: CodeBlockModelCollection, + private readonly codeBlockStartIndex: number, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IKeybindingService keybindingService: IKeybindingService, + @IModelService private readonly modelService: IModelService, + @ILanguageService private readonly languageService: ILanguageService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, + @ICommandService private readonly commandService: ICommandService, + @IMarkerService private readonly markerService: IMarkerService, + @ILanguageModelToolsService private readonly languageModelToolsService: ILanguageModelToolsService, + @IChatMarkdownAnchorService private readonly chatMarkdownAnchorService: IChatMarkdownAnchorService, + ) { + super(toolInvocation); + + if (!toolInvocation.confirmationMessages) { + throw new Error('Confirmation messages are missing'); + } + const { title, message, allowAutoConfirm } = toolInvocation.confirmationMessages; + const continueLabel = localize('continue', "Continue"); + const continueKeybinding = keybindingService.lookupKeybinding(AcceptToolConfirmationActionId)?.getLabel(); + const continueTooltip = continueKeybinding ? `${continueLabel} (${continueKeybinding})` : continueLabel; + const cancelLabel = localize('cancel', "Cancel"); + const cancelKeybinding = keybindingService.lookupKeybinding(CancelChatActionId)?.getLabel(); + const cancelTooltip = cancelKeybinding ? `${cancelLabel} (${cancelKeybinding})` : cancelLabel; + + const enum ConfirmationOutcome { + Allow, + Disallow, + AllowWorkspace, + AllowGlobally, + AllowSession, + } + + const buttons: IChatConfirmationButton[] = [ + { + label: continueLabel, + data: ConfirmationOutcome.Allow, + tooltip: continueTooltip, + moreActions: !allowAutoConfirm ? undefined : [ + { label: localize('allowSession', 'Allow in this Session'), data: ConfirmationOutcome.AllowSession, tooltip: localize('allowSesssionTooltip', 'Allow this tool to run in this session without confirmation.') }, + { label: localize('allowWorkspace', 'Allow in this Workspace'), data: ConfirmationOutcome.AllowWorkspace, tooltip: localize('allowWorkspaceTooltip', 'Allow this tool to run in this workspace without confirmation.') }, + { label: localize('allowGlobally', 'Always Allow'), data: ConfirmationOutcome.AllowGlobally, tooltip: localize('allowGloballTooltip', 'Always allow this tool to run without confirmation.') }, + ], + }, + { + label: localize('cancel', "Cancel"), + data: ConfirmationOutcome.Disallow, + isSecondary: true, + tooltip: cancelTooltip + }]; + let confirmWidget: ChatConfirmationWidget | ChatCustomConfirmationWidget; + if (typeof message === 'string') { + confirmWidget = this._register(this.instantiationService.createInstance( + ChatConfirmationWidget, + title, + toolInvocation.originMessage, + message, + buttons, + this.context.container, + )); + } else { + const chatMarkdownContent: IChatMarkdownContent = { + kind: 'markdownContent', + content: message, + }; + const codeBlockRenderOptions: ICodeBlockRenderOptions = { + hideToolbar: true, + reserveWidth: 19, + verticalPadding: 5, + editorOptions: { + wordWrap: 'on' + } + }; + + const elements = dom.h('div', [ + dom.h('.message@message'), + dom.h('.editor@editor'), + ]); + + if (toolInvocation.toolSpecificData?.kind === 'input' && toolInvocation.toolSpecificData.rawInput && !isEmptyObject(toolInvocation.toolSpecificData.rawInput)) { + + const inputData = toolInvocation.toolSpecificData; + + const codeBlockRenderOptions: ICodeBlockRenderOptions = { + hideToolbar: true, + reserveWidth: 19, + maxHeightInLines: 13, + verticalPadding: 5, + editorOptions: { + wordWrap: 'off', + readOnly: false + } + }; + + const langId = this.languageService.getLanguageIdByLanguageName('json'); + const rawJsonInput = JSON.stringify(inputData.rawInput ?? {}, null, 1); + const canSeeMore = count(rawJsonInput, '\n') > 2; // if more than one key:value + const model = this._register(this.modelService.createModel( + // View a single JSON line by default until they 'see more' + rawJsonInput.replace(/\n */g, ' '), + this.languageService.createById(langId), + createToolInputUri(toolInvocation.toolId), + true + )); + + const markerOwner = generateUuid(); + const schemaUri = createToolSchemaUri(toolInvocation.toolId); + const validator = new RunOnceScheduler(async () => { + + const newMarker: IMarkerData[] = []; + + const result = await this.commandService.executeCommand('json.validate', schemaUri, model.getValue()); + for (const item of result) { + if (item.range && item.message) { + newMarker.push({ + severity: item.severity === 'Error' ? MarkerSeverity.Error : MarkerSeverity.Warning, + message: item.message, + startLineNumber: item.range[0].line + 1, + startColumn: item.range[0].character + 1, + endLineNumber: item.range[1].line + 1, + endColumn: item.range[1].character + 1, + code: item.code ? String(item.code) : undefined + }); + } + } + + this.markerService.changeOne(markerOwner, model.uri, newMarker); + }, 500); + + validator.schedule(); + this._register(model.onDidChangeContent(() => validator.schedule())); + this._register(toDisposable(() => this.markerService.remove(markerOwner, [model.uri]))); + this._register(validator); + + const editor = this._register(this.editorPool.get()); + editor.object.render({ + codeBlockIndex: this.codeBlockStartIndex, + codeBlockPartIndex: 0, + element: this.context.element, + languageId: langId ?? 'json', + renderOptions: codeBlockRenderOptions, + textModel: Promise.resolve(model), + chatSessionId: this.context.element.sessionId + }, this.currentWidthDelegate()); + this.codeblocks.push({ + codeBlockIndex: this.codeBlockStartIndex, + codemapperUri: undefined, + elementId: this.context.element.id, + focus: () => editor.object.focus(), + isStreaming: false, + ownerMarkdownPartId: this.codeblocksPartId, + uri: model.uri, + uriPromise: Promise.resolve(model.uri), + chatSessionId: this.context.element.sessionId + }); + this._register(editor.object.onDidChangeContentHeight(() => { + editor.object.layout(this.currentWidthDelegate()); + this._onDidChangeHeight.fire(); + })); + this._register(model.onDidChangeContent(e => { + try { + inputData.rawInput = JSON.parse(model.getValue()); + } catch { + // ignore + } + })); + + elements.editor.append(editor.object.element); + + if (canSeeMore) { + const seeMore = dom.h('div.see-more', [dom.h('a@link')]); + seeMore.link.textContent = localize('seeMore', "See more"); + this._register(dom.addDisposableGenericMouseDownListener(seeMore.link, () => { + try { + const parsed = JSON.parse(model.getValue()); + model.setValue(JSON.stringify(parsed, null, 2)); + editor.object.editor.updateOptions({ wordWrap: 'on' }); + } catch { + // ignored + } + seeMore.root.remove(); + })); + elements.editor.append(seeMore.root); + } + } + + this.markdownPart = this._register(this.instantiationService.createInstance(ChatMarkdownContentPart, chatMarkdownContent, this.context, this.editorPool, false, this.codeBlockStartIndex, this.renderer, this.currentWidthDelegate(), this.codeBlockModelCollection, { codeBlockRenderOptions })); + renderFileWidgets(this.markdownPart.domNode, this.instantiationService, this.chatMarkdownAnchorService, this._store); + elements.message.append(this.markdownPart.domNode); + + this._register(this.markdownPart.onDidChangeHeight(() => this._onDidChangeHeight.fire())); + confirmWidget = this._register(this.instantiationService.createInstance( + ChatCustomConfirmationWidget, + title, + toolInvocation.originMessage, + elements.root, + buttons, + this.context.container, + )); + } + + const hasToolConfirmation = ChatContextKeys.Editing.hasToolConfirmation.bindTo(this.contextKeyService); + hasToolConfirmation.set(true); + + this._register(confirmWidget.onDidClick(button => { + switch (button.data as ConfirmationOutcome) { + case ConfirmationOutcome.AllowGlobally: + this.languageModelToolsService.setToolAutoConfirmation(toolInvocation.toolId, 'profile', true); + toolInvocation.confirmed.complete(true); + break; + case ConfirmationOutcome.AllowWorkspace: + this.languageModelToolsService.setToolAutoConfirmation(toolInvocation.toolId, 'workspace', true); + toolInvocation.confirmed.complete(true); + break; + case ConfirmationOutcome.AllowSession: + this.languageModelToolsService.setToolAutoConfirmation(toolInvocation.toolId, 'memory', true); + toolInvocation.confirmed.complete(true); + break; + case ConfirmationOutcome.Allow: + toolInvocation.confirmed.complete(true); + break; + case ConfirmationOutcome.Disallow: + toolInvocation.confirmed.complete(false); + break; + } + + this.chatWidgetService.getWidgetBySessionId(this.context.element.sessionId)?.focusInput(); + })); + this._register(confirmWidget.onDidChangeHeight(() => this._onDidChangeHeight.fire())); + this._register(toDisposable(() => hasToolConfirmation.reset())); + toolInvocation.confirmed.p.then(() => { + hasToolConfirmation.reset(); + this._onNeedsRerender.fire(); + }); + this.domNode = confirmWidget.domNode; + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatToolInvocationPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatToolInvocationPart.ts new file mode 100644 index 00000000000..5abad6f9df9 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatToolInvocationPart.ts @@ -0,0 +1,137 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from '../../../../../../base/browser/dom.js'; +import { Emitter } from '../../../../../../base/common/event.js'; +import { Disposable, DisposableStore, IDisposable } from '../../../../../../base/common/lifecycle.js'; +import { MarkdownRenderer } from '../../../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js'; +import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; +import { IChatToolInvocation, IChatToolInvocationSerialized } from '../../../common/chatService.js'; +import { IChatRendererContent } from '../../../common/chatViewModel.js'; +import { CodeBlockModelCollection } from '../../../common/codeBlockModelCollection.js'; +import { isToolResultInputOutputDetails } from '../../../common/languageModelToolsService.js'; +import { ChatTreeItem, IChatCodeBlockInfo } from '../../chat.js'; +import { IChatContentPart, IChatContentPartRenderContext } from '../chatContentParts.js'; +import { EditorPool } from '../chatMarkdownContentPart.js'; +import { CollapsibleListPool } from '../chatReferencesContentPart.js'; +import { ChatInputOutputMarkdownProgressPart } from './chatInputOutputMarkdownProgressPart.js'; +import { ChatResultListSubPart } from './chatResultListSubPart.js'; +import { ChatTerminalMarkdownProgressPart } from './chatTerminalMarkdownProgressPart.js'; +import { TerminalConfirmationWidgetSubPart } from './chatTerminalToolSubPart.js'; +import { ToolConfirmationSubPart } from './chatToolConfirmationSubPart.js'; +import { BaseChatToolInvocationSubPart } from './chatToolInvocationSubPart.js'; +import { ChatToolProgressSubPart } from './chatToolProgressPart.js'; + +export class ChatToolInvocationPart extends Disposable implements IChatContentPart { + public readonly domNode: HTMLElement; + + private _onDidChangeHeight = this._register(new Emitter()); + public readonly onDidChangeHeight = this._onDidChangeHeight.event; + + public get codeblocks(): IChatCodeBlockInfo[] { + return this.subPart?.codeblocks ?? []; + } + + public get codeblocksPartId(): string | undefined { + return this.subPart?.codeblocksPartId; + } + + private subPart!: BaseChatToolInvocationSubPart; + + constructor( + private readonly toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized, + private readonly context: IChatContentPartRenderContext, + private readonly renderer: MarkdownRenderer, + private readonly listPool: CollapsibleListPool, + private readonly editorPool: EditorPool, + private readonly currentWidthDelegate: () => number, + private readonly codeBlockModelCollection: CodeBlockModelCollection, + private readonly codeBlockStartIndex: number, + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { + super(); + + this.domNode = dom.$('.chat-tool-invocation-part'); + if (toolInvocation.presentation === 'hidden') { + return; + } + + // This part is a bit different, since IChatToolInvocation is not an immutable model object. So this part is able to rerender itself. + // If this turns out to be a typical pattern, we could come up with a more reusable pattern, like telling the list to rerender an element + // when the model changes, or trying to make the model immutable and swap out one content part for a new one based on user actions in the view. + const partStore = this._register(new DisposableStore()); + const render = () => { + dom.clearNode(this.domNode); + partStore.clear(); + + this.subPart = partStore.add(this.createToolInvocationSubPart()); + this.domNode.appendChild(this.subPart.domNode); + partStore.add(this.subPart.onDidChangeHeight(() => this._onDidChangeHeight.fire())); + partStore.add(this.subPart.onNeedsRerender(() => { + render(); + this._onDidChangeHeight.fire(); + })); + }; + render(); + } + + createToolInvocationSubPart(): BaseChatToolInvocationSubPart { + if (this.toolInvocation.kind === 'toolInvocation' && this.toolInvocation.confirmationMessages) { + if (this.toolInvocation.toolSpecificData?.kind === 'terminal') { + return this.instantiationService.createInstance(TerminalConfirmationWidgetSubPart, this.toolInvocation, this.toolInvocation.toolSpecificData, this.context, this.renderer, this.editorPool, this.currentWidthDelegate, this.codeBlockStartIndex); + } else { + return this.instantiationService.createInstance(ToolConfirmationSubPart, this.toolInvocation, this.context, this.renderer, this.editorPool, this.currentWidthDelegate, this.codeBlockModelCollection, this.codeBlockStartIndex); + } + } + + if (this.toolInvocation.toolSpecificData?.kind === 'terminal') { + return this.instantiationService.createInstance(ChatTerminalMarkdownProgressPart, this.toolInvocation, this.toolInvocation.toolSpecificData, this.context, this.renderer, this.editorPool, this.currentWidthDelegate, this.codeBlockStartIndex, this.codeBlockModelCollection); + } + + if (Array.isArray(this.toolInvocation.resultDetails) && this.toolInvocation.resultDetails?.length) { + return this.instantiationService.createInstance(ChatResultListSubPart, this.toolInvocation, this.context, this.toolInvocation.pastTenseMessage ?? this.toolInvocation.invocationMessage, this.toolInvocation.resultDetails, this.listPool); + } + + if (isToolResultInputOutputDetails(this.toolInvocation.resultDetails)) { + return this.instantiationService.createInstance( + ChatInputOutputMarkdownProgressPart, + this.toolInvocation, + this.context, + this.editorPool, + this.codeBlockStartIndex, + this.toolInvocation.pastTenseMessage ?? this.toolInvocation.invocationMessage, + this.toolInvocation.originMessage, + this.toolInvocation.resultDetails.input, + this.toolInvocation.resultDetails.output, + !!this.toolInvocation.resultDetails.isError + ); + } + + if (this.toolInvocation.kind === 'toolInvocation' && this.toolInvocation.toolSpecificData?.kind === 'input' && !this.toolInvocation.isComplete) { + return this.instantiationService.createInstance( + ChatInputOutputMarkdownProgressPart, + this.toolInvocation, + this.context, + this.editorPool, + this.codeBlockStartIndex, + this.toolInvocation.invocationMessage, + this.toolInvocation.originMessage, + typeof this.toolInvocation.toolSpecificData.rawInput === 'string' ? this.toolInvocation.toolSpecificData.rawInput : JSON.stringify(this.toolInvocation.toolSpecificData.rawInput, null, 2), + undefined, + false + ); + } + + return this.instantiationService.createInstance(ChatToolProgressSubPart, this.toolInvocation, this.context, this.renderer); + } + + hasSameContent(other: IChatRendererContent, followingContent: IChatRendererContent[], element: ChatTreeItem): boolean { + return (other.kind === 'toolInvocation' || other.kind === 'toolInvocationSerialized') && this.toolInvocation.toolCallId === other.toolCallId; + } + + addDisposable(disposable: IDisposable): void { + this._register(disposable); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatToolInvocationSubPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatToolInvocationSubPart.ts new file mode 100644 index 00000000000..56be77b7005 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatToolInvocationSubPart.ts @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter } from '../../../../../../base/common/event.js'; +import { Disposable } from '../../../../../../base/common/lifecycle.js'; +import { IChatToolInvocation, IChatToolInvocationSerialized } from '../../../common/chatService.js'; +import { IChatCodeBlockInfo } from '../../chat.js'; + +export abstract class BaseChatToolInvocationSubPart extends Disposable { + protected static idPool = 0; + public abstract readonly domNode: HTMLElement; + + protected _onNeedsRerender = this._register(new Emitter()); + public readonly onNeedsRerender = this._onNeedsRerender.event; + + protected _onDidChangeHeight = this._register(new Emitter()); + public readonly onDidChangeHeight = this._onDidChangeHeight.event; + + public abstract codeblocks: IChatCodeBlockInfo[]; + + public readonly codeblocksPartId = 'tool-' + (BaseChatToolInvocationSubPart.idPool++); + + constructor( + toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized, + ) { + super(); + + if (toolInvocation.kind === 'toolInvocation' && !toolInvocation.isComplete) { + toolInvocation.isCompletePromise.then(() => this._onNeedsRerender.fire()); + } + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatToolProgressPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatToolProgressPart.ts new file mode 100644 index 00000000000..89d07da2a06 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatToolProgressPart.ts @@ -0,0 +1,67 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from '../../../../../../base/browser/dom.js'; +import { Codicon } from '../../../../../../base/common/codicons.js'; +import { IMarkdownString, MarkdownString } from '../../../../../../base/common/htmlContent.js'; +import { autorun } from '../../../../../../base/common/observable.js'; +import { MarkdownRenderer } from '../../../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js'; +import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; +import { IChatToolInvocation, IChatToolInvocationSerialized, IChatProgressMessage } from '../../../common/chatService.js'; +import { IChatCodeBlockInfo } from '../../chat.js'; +import { IChatContentPartRenderContext } from '../chatContentParts.js'; +import { ChatProgressContentPart } from '../chatProgressContentPart.js'; +import { BaseChatToolInvocationSubPart } from './chatToolInvocationSubPart.js'; + +export class ChatToolProgressSubPart extends BaseChatToolInvocationSubPart { + public readonly domNode: HTMLElement; + + public override readonly codeblocks: IChatCodeBlockInfo[] = []; + + constructor( + private readonly toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized, + private readonly context: IChatContentPartRenderContext, + private readonly renderer: MarkdownRenderer, + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { + super(toolInvocation); + + this.domNode = this.createProgressPart(); + } + + private createProgressPart(): HTMLElement { + if (this.toolInvocation.isComplete && this.toolInvocation.isConfirmed !== false && this.toolInvocation.pastTenseMessage) { + const part = this.renderProgressContent(this.toolInvocation.pastTenseMessage); + this._register(part); + return part.domNode; + } else { + const container = document.createElement('div'); + const progressObservable = this.toolInvocation.kind === 'toolInvocation' ? this.toolInvocation.progress : undefined; + this._register(autorun(reader => { + const progress = progressObservable?.read(reader); + const part = reader.store.add(this.renderProgressContent(progress?.message || this.toolInvocation.invocationMessage)); + dom.reset(container, part.domNode); + })); + return container; + } + } + + private renderProgressContent(content: IMarkdownString | string) { + if (typeof content === 'string') { + content = new MarkdownString().appendText(content); + } + + const progressMessage: IChatProgressMessage = { + kind: 'progressMessage', + content + }; + + const iconOverride = !this.toolInvocation.isConfirmed ? + Codicon.error : + this.toolInvocation.isComplete ? + Codicon.check : undefined; + return this.instantiationService.createInstance(ChatProgressContentPart, progressMessage, this.renderer, this.context, undefined, true, iconOverride); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatContextPickService.ts b/src/vs/workbench/contrib/chat/browser/chatContextPickService.ts new file mode 100644 index 00000000000..a1c1a65b904 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatContextPickService.ts @@ -0,0 +1,96 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; +import { ThemeIcon } from '../../../../base/common/themables.js'; +import { isObject } from '../../../../base/common/types.js'; +import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; +import { IQuickPickSeparator } from '../../../../platform/quickinput/common/quickInput.js'; +import { IChatWidget } from './chat.js'; +import { IChatRequestVariableEntry } from '../common/chatModel.js'; +import { compare } from '../../../../base/common/strings.js'; + + +export interface IChatContextPickerPickItem { + label: string; + iconClass?: string; + description?: string; + disabled?: boolean; + asAttachment(): IChatRequestVariableEntry; +} + +export function isChatContextPickerPickItem(item: unknown): item is IChatContextPickerPickItem { + return isObject(item) && typeof (item as IChatContextPickerPickItem).asAttachment === 'function'; +} + +interface IChatContextItem { + readonly label: string; + readonly icon: ThemeIcon; + readonly commandId?: string; + readonly ordinal?: number; + isEnabled?(widget: IChatWidget): Promise | boolean; +} + +export interface IChatContextValueItem extends IChatContextItem { + readonly type: 'valuePick'; + + asAttachment(widget: IChatWidget): Promise; +} + +export interface IChatContextPickerItem extends IChatContextItem { + readonly type: 'pickerPick'; + + asPicker(widget: IChatWidget): { + readonly placeholder: string; + readonly picks: Promise<(IChatContextPickerPickItem | IQuickPickSeparator)[]> | ((query: string, token: CancellationToken) => Promise<(IChatContextPickerPickItem | IQuickPickSeparator)[]>); + }; +} + +export interface IChatContextPickService { + _serviceBrand: undefined; + + items: Iterable; + + /** + * Register a value or picker to the "Add Context" flow. A value directly resolved to a + * chat attachment and a picker first shows a list of items to pick from and then + * resolves the selected item to a chat attachment. + */ + registerChatContextItem(item: IChatContextValueItem | IChatContextPickerItem): IDisposable; +} + +export const IChatContextPickService = createDecorator('IContextPickService'); + +export class ChatContextPickService implements IChatContextPickService { + + declare _serviceBrand: undefined; + + private readonly _picks: IChatContextValueItem[] = []; + + readonly items: Iterable = this._picks; + + registerChatContextItem(pick: IChatContextValueItem): IDisposable { + this._picks.push(pick); + + this._picks.sort((a, b) => { + const valueA = a.ordinal ?? 0; + const valueB = b.ordinal ?? 0; + if (valueA === valueB) { + return compare(a.label, b.label); + } else if (valueA < valueB) { + return 1; + } else { + return -1; + } + }); + + return toDisposable(() => { + const index = this._picks.indexOf(pick); + if (index >= 0) { + this._picks.splice(index, 1); + } + }); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts index bc7590c5bce..f50b6b77b57 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts @@ -608,7 +608,7 @@ registerAction2(class ResolveSymbolsContextAction extends EditingSessionAction { // how important it is that they make it into the working set as it has limited size const attachments = []; for (const reference of [...definitions, ...implementations, ...references]) { - attachments.push(chatWidget.attachmentModel.asVariableEntry(reference.uri)); + attachments.push(chatWidget.attachmentModel.asFileVariableEntry(reference.uri)); } chatWidget.attachmentModel.addContext(...attachments); diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingCodeEditorIntegration.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingCodeEditorIntegration.ts index 156b0102446..c272672658d 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingCodeEditorIntegration.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingCodeEditorIntegration.ts @@ -8,7 +8,7 @@ import '../media/chatEditorController.css'; import { getTotalWidth } from '../../../../../base/browser/dom.js'; import { Event } from '../../../../../base/common/event.js'; import { DisposableStore, dispose, toDisposable } from '../../../../../base/common/lifecycle.js'; -import { autorun, autorunWithStore, constObservable, derived, IObservable, observableFromEvent, observableValue } from '../../../../../base/common/observable.js'; +import { autorun, constObservable, derived, IObservable, observableFromEvent, observableValue } from '../../../../../base/common/observable.js'; import { basename, isEqual } from '../../../../../base/common/resources.js'; import { themeColorFromId } from '../../../../../base/common/themables.js'; import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, IOverlayWidgetPositionCoordinates, IViewZone, MouseTargetType } from '../../../../../editor/browser/editorBrowser.js'; @@ -180,7 +180,7 @@ export class ChatEditingCodeEditorIntegration implements IModifiedFileEntryEdito })); // accessibility: diff view - this._store.add(autorunWithStore((r, store) => { + this._store.add(autorun(r => { const visible = this._accessibleDiffViewVisible.read(r); @@ -190,9 +190,9 @@ export class ChatEditingCodeEditorIntegration implements IModifiedFileEntryEdito const accessibleDiffWidget = new AccessibleDiffViewContainer(); _editor.addOverlayWidget(accessibleDiffWidget); - store.add(toDisposable(() => _editor.removeOverlayWidget(accessibleDiffWidget))); + r.store.add(toDisposable(() => _editor.removeOverlayWidget(accessibleDiffWidget))); - store.add(instantiationService.createInstance( + r.store.add(instantiationService.createInstance( AccessibleDiffViewer, accessibleDiffWidget.getDomNode(), enabledObs, diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedDocumentEntry.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedDocumentEntry.ts index 034cb8395fe..4c06e82bfa7 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedDocumentEntry.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedDocumentEntry.ts @@ -13,7 +13,7 @@ import { assertType } from '../../../../../base/common/types.js'; import { URI } from '../../../../../base/common/uri.js'; import { getCodeEditor } from '../../../../../editor/browser/editorBrowser.js'; import { EditOperation, ISingleEditOperation } from '../../../../../editor/common/core/editOperation.js'; -import { OffsetEdit } from '../../../../../editor/common/core/edits/offsetEdit.js'; +import { StringEdit } from '../../../../../editor/common/core/edits/stringEdit.js'; import { Range } from '../../../../../editor/common/core/range.js'; import { IDocumentDiff, nullDocumentDiff } from '../../../../../editor/common/diff/documentDiffProvider.js'; import { DetailedLineRangeMapping } from '../../../../../editor/common/diff/rangeMapping.js'; @@ -22,7 +22,7 @@ import { ILanguageService } from '../../../../../editor/common/languages/languag import { IModelDeltaDecoration, ITextModel, MinimapPosition, OverviewRulerLane } from '../../../../../editor/common/model.js'; import { SingleModelEditStackElement } from '../../../../../editor/common/model/editStack.js'; import { ModelDecorationOptions, createTextBufferFactoryFromSnapshot } from '../../../../../editor/common/model/textModel.js'; -import { OffsetEdits } from '../../../../../editor/common/model/textModelOffsetEdit.js'; +import { offsetEditFromContentChanges, offsetEditFromLineRangeMapping, offsetEditToEditOperations } from '../../../../../editor/common/model/textModelStringEdit.js'; import { IEditorWorkerService } from '../../../../../editor/common/services/editorWorker.js'; import { IModelService } from '../../../../../editor/common/services/model.js'; import { IResolvedTextEditorModel, ITextModelService } from '../../../../../editor/common/services/resolverService.js'; @@ -78,7 +78,7 @@ export class ChatEditingModifiedDocumentEntry extends AbstractChatEditingModifie private readonly _docFileEditorModel: IResolvedTextEditorModel; - private _edit: OffsetEdit = OffsetEdit.empty; + private _edit: StringEdit = StringEdit.empty; private _isEditFromUs: boolean = false; private _allEditsAreFromUs: boolean = true; private _diffOperation: Promise | undefined; @@ -221,7 +221,7 @@ export class ChatEditingModifiedDocumentEntry extends AbstractChatEditingModifie } private _mirrorEdits(event: IModelContentChangedEvent) { - const edit = OffsetEdits.fromContentChanges(event.changes); + const edit = offsetEditFromContentChanges(event.changes); if (this._isEditFromUs) { const e_sum = this._edit; @@ -253,7 +253,7 @@ export class ChatEditingModifiedDocumentEntry extends AbstractChatEditingModifie // user edits overlaps/conflicts with AI edits this._edit = e_ai.compose(e_user); } else { - const edits = OffsetEdits.asEditOperations(e_user_r, this.originalModel); + const edits = offsetEditToEditOperations(e_user_r, this.originalModel); this.originalModel.applyEdits(edits); this._edit = e_ai.tryRebase(e_user_r); } @@ -431,7 +431,7 @@ export class ChatEditingModifiedDocumentEntry extends AbstractChatEditingModifie if (this.modifiedModel.getVersionId() === docVersionNow && this.originalModel.getVersionId() === snapshotVersionNow) { const diff2 = diff ?? nullDocumentDiff; this._diffInfo.set(diff2, undefined); - this._edit = OffsetEdits.fromLineRangeMapping(this.originalModel, this.modifiedModel, diff2.changes); + this._edit = offsetEditFromLineRangeMapping(this.originalModel, this.modifiedModel, diff2.changes); return diff2; } return undefined; @@ -440,7 +440,7 @@ export class ChatEditingModifiedDocumentEntry extends AbstractChatEditingModifie protected override async _doAccept(tx: ITransaction | undefined): Promise { this.originalModel.setValue(this.modifiedModel.createSnapshot()); this._diffInfo.set(nullDocumentDiff, tx); - this._edit = OffsetEdit.empty; + this._edit = StringEdit.empty; await this._collapse(tx); const config = this._fileConfigService.getAutoSaveConfiguration(this.modifiedURI); diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts index eef68bc8571..97863aa1625 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts @@ -10,7 +10,7 @@ import { Schemas } from '../../../../../base/common/network.js'; import { clamp } from '../../../../../base/common/numbers.js'; import { autorun, derived, IObservable, ITransaction, observableValue, observableValueOpts } from '../../../../../base/common/observable.js'; import { URI } from '../../../../../base/common/uri.js'; -import { OffsetEdit } from '../../../../../editor/common/core/edits/offsetEdit.js'; +import { StringEdit } from '../../../../../editor/common/core/edits/stringEdit.js'; import { TextEdit } from '../../../../../editor/common/languages.js'; import { localize } from '../../../../../nls.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; @@ -325,7 +325,7 @@ export interface ISnapshotEntry { readonly snapshotUri: URI; readonly original: string; readonly current: string; - readonly originalToCurrentEdit: OffsetEdit; + readonly originalToCurrentEdit: StringEdit; readonly state: ModifiedFileEntryState; telemetryInfo: IModifiedEntryTelemetryInfo; } diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts index f52ef2821cf..856a76c73bd 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts @@ -15,7 +15,7 @@ import { assertType } from '../../../../../base/common/types.js'; import { URI } from '../../../../../base/common/uri.js'; import { generateUuid } from '../../../../../base/common/uuid.js'; import { LineRange } from '../../../../../editor/common/core/ranges/lineRange.js'; -import { OffsetEdit } from '../../../../../editor/common/core/edits/offsetEdit.js'; +import { StringEdit } from '../../../../../editor/common/core/edits/stringEdit.js'; import { Range } from '../../../../../editor/common/core/range.js'; import { nullDocumentDiff } from '../../../../../editor/common/diff/documentDiffProvider.js'; import { DetailedLineRangeMapping, RangeMapping } from '../../../../../editor/common/diff/rangeMapping.js'; @@ -475,7 +475,7 @@ export class ChatEditingModifiedNotebookEntry extends AbstractChatEditingModifie protected override _resetEditsState(tx: ITransaction): void { super._resetEditsState(tx); - this.cellEntryMap.forEach(entry => !entry.disposed && entry.clearCurrentEditLineDecoration()); + this.cellEntryMap.forEach(entry => !entry.isDisposed && entry.clearCurrentEditLineDecoration()); } protected override _createUndoRedoElement(response: IChatResponseModel): IUndoRedoElement | undefined { @@ -918,7 +918,7 @@ export class ChatEditingModifiedNotebookEntry extends AbstractChatEditingModifie snapshotUri: getNotebookSnapshotFileURI(this._telemetryInfo.sessionId, requestId, undoStop, this.modifiedURI.path, this.modifiedModel.viewType), original: createSnapshot(this.originalModel, this.transientOptions, this.configurationService), current: createSnapshot(this.modifiedModel, this.transientOptions, this.configurationService), - originalToCurrentEdit: OffsetEdit.empty, + originalToCurrentEdit: StringEdit.empty, state: this.state.get(), telemetryInfo: this.telemetryInfo, }; diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts index 029acf012b2..bdd8db81925 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts @@ -404,16 +404,17 @@ class ChatDecorationsProvider extends Disposable implements IDecorationsProvider return uri.filter(entry => !entry.isCurrentlyBeingModifiedBy.read(r) && entry.state.read(r) === ModifiedFileEntryState.Modified).map(entry => entry.modifiedURI); }); - public readonly onDidChange = Event.any( - observeArrayChanges(this._currentlyEditingUris, compareBy(uri => uri.toString(), compare), this._store), - observeArrayChanges(this._modifiedUris, compareBy(uri => uri.toString(), compare), this._store), - ); + readonly onDidChange: Event; constructor( private readonly _sessions: IObservable, @IChatAgentService private readonly _chatAgentService: IChatAgentService ) { super(); + this.onDidChange = Event.any( + observeArrayChanges(this._currentlyEditingUris, compareBy(uri => uri.toString(), compare), this._store), + observeArrayChanges(this._modifiedUris, compareBy(uri => uri.toString(), compare), this._store), + ); } provideDecorations(uri: URI, _token: CancellationToken): IDecorationData | undefined { diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts index d07f8f070bc..ec6fd1c455d 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts @@ -10,9 +10,9 @@ import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { BugIndicatingError } from '../../../../../base/common/errors.js'; import { Emitter } from '../../../../../base/common/event.js'; import { Iterable } from '../../../../../base/common/iterator.js'; -import { Disposable, dispose } from '../../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, dispose } from '../../../../../base/common/lifecycle.js'; import { ResourceMap } from '../../../../../base/common/map.js'; -import { asyncTransaction, autorun, derived, derivedOpts, derivedWithStore, IObservable, IReader, ITransaction, ObservablePromise, observableValue, transaction } from '../../../../../base/common/observable.js'; +import { asyncTransaction, autorun, derived, derivedOpts, IObservable, IReader, ITransaction, ObservablePromise, observableValue, transaction } from '../../../../../base/common/observable.js'; import { isEqual } from '../../../../../base/common/resources.js'; import { URI } from '../../../../../base/common/uri.js'; import { IBulkEditService } from '../../../../../editor/browser/services/bulkEditService.js'; @@ -300,10 +300,11 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio entriesContent: IObservable<{ before: ISnapshotEntry; after: ISnapshotEntry } | undefined>, modelUrisObservable: IObservable<[URI, URI] | undefined>, ): IObservable | undefined> { - const modelRefsPromise = derivedWithStore(this, (reader, store) => { + const modelRefsPromise = derived(this, (reader) => { const modelUris = modelUrisObservable.read(reader); if (!modelUris) { return undefined; } + const store = reader.store.add(new DisposableStore()); const promise = Promise.all(modelUris.map(u => this._textModelService.createModelReference(u))).then(refs => { if (store.isDisposed) { refs.forEach(r => r.dispose()); diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSessionStorage.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSessionStorage.ts index d28f865976e..b44fc546d1f 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSessionStorage.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSessionStorage.ts @@ -8,7 +8,7 @@ import { StringSHA1 } from '../../../../../base/common/hash.js'; import { ResourceMap } from '../../../../../base/common/map.js'; import { joinPath } from '../../../../../base/common/resources.js'; import { URI } from '../../../../../base/common/uri.js'; -import { OffsetEdit, ISingleOffsetEdit, IOffsetEdit } from '../../../../../editor/common/core/edits/offsetEdit.js'; +import { StringEdit, ISerializedStringEdit } from '../../../../../editor/common/core/edits/stringEdit.js'; import { IEnvironmentService } from '../../../../../platform/environment/common/environment.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; @@ -80,7 +80,7 @@ export class ChatEditingSessionStorage { languageId: entry.languageId, original: await getFileContent(entry.originalHash), current: await getFileContent(entry.currentHash), - originalToCurrentEdit: OffsetEdit.fromJson(entry.originalToCurrentEdit), + originalToCurrentEdit: StringEdit.fromJson(entry.originalToCurrentEdit), state: entry.state, snapshotUri: URI.parse(entry.snapshotUri), telemetryInfo: { requestId: entry.telemetryInfo.requestId, agentId: entry.telemetryInfo.agentId, command: entry.telemetryInfo.command, sessionId: this.chatSessionId, result: undefined } @@ -180,7 +180,7 @@ export class ChatEditingSessionStorage { languageId: entry.languageId, originalHash: addFileContent(entry.original), currentHash: addFileContent(entry.current), - originalToCurrentEdit: entry.originalToCurrentEdit.edits.map(edit => ({ pos: edit.replaceRange.start, len: edit.replaceRange.length, txt: edit.newText } satisfies ISingleOffsetEdit)), + originalToCurrentEdit: entry.originalToCurrentEdit.toJson(), state: entry.state, snapshotUri: entry.snapshotUri.toString(), telemetryInfo: { requestId: entry.telemetryInfo.requestId, agentId: entry.telemetryInfo.agentId, command: entry.telemetryInfo.command } @@ -275,7 +275,7 @@ interface ISnapshotEntryDTO { readonly languageId: string; readonly originalHash: string; readonly currentHash: string; - readonly originalToCurrentEdit: IOffsetEdit; + readonly originalToCurrentEdit: ISerializedStringEdit; readonly state: ModifiedFileEntryState; readonly snapshotUri: string; readonly telemetryInfo: IModifiedEntryTelemetryInfoDTO; diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingNotebookCellEntry.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingNotebookCellEntry.ts index 17e5ce86dc1..0913bbf6a92 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingNotebookCellEntry.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingNotebookCellEntry.ts @@ -10,14 +10,14 @@ import { ObservableDisposable } from '../../../../../../base/common/observableDi import { themeColorFromId } from '../../../../../../base/common/themables.js'; import { URI } from '../../../../../../base/common/uri.js'; import { EditOperation, ISingleEditOperation } from '../../../../../../editor/common/core/editOperation.js'; -import { OffsetEdit } from '../../../../../../editor/common/core/edits/offsetEdit.js'; +import { StringEdit } from '../../../../../../editor/common/core/edits/stringEdit.js'; import { Range } from '../../../../../../editor/common/core/range.js'; import { IDocumentDiff, nullDocumentDiff } from '../../../../../../editor/common/diff/documentDiffProvider.js'; import { DetailedLineRangeMapping } from '../../../../../../editor/common/diff/rangeMapping.js'; import { TextEdit } from '../../../../../../editor/common/languages.js'; import { IModelDeltaDecoration, ITextModel, MinimapPosition, OverviewRulerLane } from '../../../../../../editor/common/model.js'; import { ModelDecorationOptions } from '../../../../../../editor/common/model/textModel.js'; -import { OffsetEdits } from '../../../../../../editor/common/model/textModelOffsetEdit.js'; +import { offsetEditFromContentChanges, offsetEditFromLineRangeMapping, offsetEditToEditOperations } from '../../../../../../editor/common/model/textModelStringEdit.js'; import { IEditorWorkerService } from '../../../../../../editor/common/services/editorWorker.js'; import { IModelContentChangedEvent } from '../../../../../../editor/common/textModelEvents.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; @@ -60,7 +60,7 @@ export class ChatEditingNotebookCellEntry extends ObservableDisposable { }); - private _edit: OffsetEdit = OffsetEdit.empty; + private _edit: StringEdit = StringEdit.empty; private _isEditFromUs: boolean = false; public get isEditFromUs(): boolean { return this._isEditFromUs; @@ -126,7 +126,7 @@ export class ChatEditingNotebookCellEntry extends ObservableDisposable { private _mirrorEdits(event: IModelContentChangedEvent) { - const edit = OffsetEdits.fromContentChanges(event.changes); + const edit = offsetEditFromContentChanges(event.changes); if (this._isEditFromUs) { const e_sum = this._edit; @@ -159,7 +159,7 @@ export class ChatEditingNotebookCellEntry extends ObservableDisposable { // user edits overlaps/conflicts with AI edits this._edit = e_ai.compose(e_user); } else { - const edits = OffsetEdits.asEditOperations(e_user_r, this.originalModel); + const edits = offsetEditToEditOperations(e_user_r, this.originalModel); this.originalModel.applyEdits(edits); this._edit = e_ai.tryRebase(e_user_r); } @@ -357,7 +357,7 @@ export class ChatEditingNotebookCellEntry extends ObservableDisposable { if (this.modifiedModel.getVersionId() === docVersionNow && this.originalModel.getVersionId() === snapshotVersionNow) { const diff2 = diff ?? nullDocumentDiff; this._diffInfo.set(diff2, undefined); - this._edit = OffsetEdits.fromLineRangeMapping(this.originalModel, this.modifiedModel, diff2.changes); + this._edit = offsetEditFromLineRangeMapping(this.originalModel, this.modifiedModel, diff2.changes); } } } diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/simpleBrowserEditorOverlay.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/simpleBrowserEditorOverlay.ts index 1d8f05a8739..bc26373bca5 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/simpleBrowserEditorOverlay.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/simpleBrowserEditorOverlay.ts @@ -22,7 +22,7 @@ import { CancellationTokenSource } from '../../../../../base/common/cancellation import { IHostService } from '../../../../services/host/browser/host.js'; import { IChatWidgetService, showChatView } from '../chat.js'; import { IViewsService } from '../../../../services/views/common/viewsService.js'; -import { Button } from '../../../../../base/browser/ui/button/button.js'; +import { Button, ButtonWithDropdown } from '../../../../../base/browser/ui/button/button.js'; import { defaultButtonStyles } from '../../../../../platform/theme/browser/defaultStyles.js'; import { addDisposableListener } from '../../../../../base/browser/dom.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; @@ -34,6 +34,8 @@ import { ILogService } from '../../../../../platform/log/common/log.js'; import { IChatRequestVariableEntry } from '../../common/chatModel.js'; import { IPreferencesService } from '../../../../services/preferences/common/preferences.js'; import { IBrowserElementsService } from '../../../../services/browserElements/browser/browserElementsService.js'; +import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js'; +import { IAction, toAction } from '../../../../../base/common/actions.js'; class SimpleBrowserOverlayWidget { @@ -57,6 +59,7 @@ class SimpleBrowserOverlayWidget { @IConfigurationService private readonly configurationService: IConfigurationService, @IPreferencesService private readonly _preferencesService: IPreferencesService, @IBrowserElementsService private readonly _browserElementsService: IBrowserElementsService, + @IContextMenuService private readonly contextMenuService: IContextMenuService, ) { this._showStore.add(this.configurationService.onDidChangeConfiguration(e => { @@ -81,13 +84,59 @@ class SimpleBrowserOverlayWidget { this._domNode.appendChild(message); let cts: CancellationTokenSource; - const selectButton = this._showStore.add(new Button(this._domNode, { ...defaultButtonStyles, supportIcons: true, title: localize('selectAnElement', 'Click to select an element.') })); - selectButton.element.className = 'element-selection-start'; - selectButton.label = localize('startSelection', 'Start'); + const actions: IAction[] = []; + actions.push( + toAction({ + id: 'singleSelection', + label: localize('selectElementDropdown', 'Select an Element'), + enabled: true, + run: async () => { await startElementSelection(); } + }), + toAction({ + id: 'continuousSelection', + label: localize('continuousSelectionDropdown', 'Continuous Selection'), + enabled: true, + run: async () => { + this._editor.focus(); + cts = new CancellationTokenSource(); + // start selection + message.textContent = localize('elementSelectionInProgress', 'Selecting element...'); + this.hideElement(startButton.element); + this.showElement(cancelButton.element); + cancelButton.label = localize('finishSelectionLabel', 'Done'); + while (!cts.token.isCancellationRequested) { + try { + await this.addElementToChat(cts); + } catch (err) { + this.logService.error('Failed to select this element.', err); + cts.cancel(); + break; + } + } + + // stop selection + message.textContent = localize('elementSelectionComplete', 'Element added to chat'); + finishedSelecting(); + } + })); + + const startButton = this._showStore.add(new ButtonWithDropdown(this._domNode, { + actions: actions, + addPrimaryActionToDropdown: false, + contextMenuProvider: this.contextMenuService, + supportShortLabel: true, + title: localize('selectAnElement', 'Click to select an element.'), + supportIcons: true, + ...defaultButtonStyles + })); + + startButton.primaryButton.label = localize('startSelection', 'Start'); + startButton.element.classList.add('element-selection-start'); const cancelButton = this._showStore.add(new Button(this._domNode, { ...defaultButtonStyles, supportIcons: true, title: localize('cancelSelection', 'Click to cancel selection.') })); cancelButton.element.className = 'element-selection-cancel hidden'; - cancelButton.label = localize('cancel', 'Cancel'); + const cancelButtonLabel = localize('cancelSelectionLabel', 'Cancel'); + cancelButton.label = cancelButtonLabel; const configure = this._showStore.add(new Button(this._domNode, { supportIcons: true, title: localize('chat.configureElements', "Configure Attachments Sent") })); configure.icon = Codicon.gear; @@ -109,13 +158,14 @@ class SimpleBrowserOverlayWidget { const resetButtons = () => { this.hideElement(nextSelection.element); - this.showElement(selectButton.element); + this.showElement(startButton.element); this.showElement(collapseOverlay.element); }; const finishedSelecting = () => { // stop selection this.hideElement(cancelButton.element); + cancelButton.label = cancelButtonLabel; this.hideElement(collapseOverlay.element); this.showElement(nextSelection.element); @@ -126,19 +176,23 @@ class SimpleBrowserOverlayWidget { }, 3000); }; - this._showStore.add(addDisposableListener(selectButton.element, 'click', async () => { + const startElementSelection = async () => { cts = new CancellationTokenSource(); this._editor.focus(); // start selection message.textContent = localize('elementSelectionInProgress', 'Selecting element...'); - this.hideElement(selectButton.element); + this.hideElement(startButton.element); this.showElement(cancelButton.element); await this.addElementToChat(cts); // stop selection message.textContent = localize('elementSelectionComplete', 'Element added to chat'); finishedSelecting(); + }; + + this._showStore.add(addDisposableListener(startButton.primaryButton.element, 'click', async () => { + await startElementSelection(); })); this._showStore.add(addDisposableListener(cancelButton.element, 'click', () => { diff --git a/src/vs/workbench/contrib/chat/browser/chatInlineAnchorWidget.ts b/src/vs/workbench/contrib/chat/browser/chatInlineAnchorWidget.ts index 6aa40eb87b8..655f970726c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInlineAnchorWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInlineAnchorWidget.ts @@ -7,7 +7,7 @@ import * as dom from '../../../../base/browser/dom.js'; import { StandardMouseEvent } from '../../../../base/browser/mouseEvent.js'; import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js'; import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; -import { Disposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; import { URI } from '../../../../base/common/uri.js'; import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; import { IRange } from '../../../../editor/common/core/range.js'; @@ -36,6 +36,7 @@ import { FolderThemeIcon, IThemeService } from '../../../../platform/theme/commo import { fillEditorsDragData } from '../../../browser/dnd.js'; import { ResourceContextKey } from '../../../common/contextkeys.js'; import { IEditorService, SIDE_GROUP } from '../../../services/editor/common/editorService.js'; +import { INotebookDocumentService } from '../../../services/notebook/common/notebookDocumentService.js'; import { ExplorerFolderContext } from '../../files/common/files.js'; import { IWorkspaceSymbol } from '../../search/common/search.js'; import { IChatContentInlineReference } from '../common/chatService.js'; @@ -52,6 +53,21 @@ type ContentRefData = readonly range?: IRange; }; +export function renderFileWidgets(element: HTMLElement, instantiationService: IInstantiationService, chatMarkdownAnchorService: IChatMarkdownAnchorService, disposables: DisposableStore) { + const links = element.querySelectorAll('a'); + links.forEach(a => { + // Empty link text -> render file widget + if (!a.textContent?.trim()) { + const href = a.getAttribute('data-href'); + const uri = href ? URI.parse(href) : undefined; + if (uri?.scheme) { + const widget = instantiationService.createInstance(InlineAnchorWidget, a, { kind: 'inlineReference', inlineReference: uri }); + disposables.add(chatMarkdownAnchorService.register(widget)); + } + } + }); +} + export class InlineAnchorWidget extends Disposable { public static readonly className = 'chat-inline-anchor-widget'; @@ -76,6 +92,7 @@ export class InlineAnchorWidget extends Disposable { @IModelService modelService: IModelService, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, + @INotebookDocumentService private readonly notebookDocumentService: INotebookDocumentService, ) { super(); @@ -112,7 +129,9 @@ export class InlineAnchorWidget extends Disposable { const label = labelService.getUriBasenameLabel(location.uri); iconText = location.range && this.data.kind !== 'symbol' ? `${label}#${location.range.startLineNumber}-${location.range.endLineNumber}` : - label; + location.uri.scheme === 'vscode-notebook-cell' && this.data.kind !== 'symbol' ? + `${label} • cell${this.getCellIndex(location.uri)}` : + label; let fileKind = location.uri.path.endsWith('/') ? FileKind.FOLDER : FileKind.FILE; const recomputeIconClasses = () => getIconClasses(modelService, languageService, location.uri, fileKind, fileKind === FileKind.FOLDER && !themeService.getFileIconTheme().hasFolderIcons ? FolderThemeIcon : undefined); @@ -205,6 +224,12 @@ export class InlineAnchorWidget extends Disposable { getHTMLElement(): HTMLElement { return this.element; } + + private getCellIndex(location: URI) { + const notebook = this.notebookDocumentService.getNotebook(location); + const index = notebook?.getCellIndex(location) ?? -1; + return index >= 0 ? ` ${index + 1}` : ''; + } } //#region Resource context menu diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index f612856d89a..59dd28bc75a 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -16,7 +16,7 @@ import { IAction } from '../../../../base/common/actions.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { logExecutionTime } from '../../../../base/common/decorators/logTime.js'; -import { Emitter } from '../../../../base/common/event.js'; +import { Emitter, Event } from '../../../../base/common/event.js'; import { HistoryNavigator2 } from '../../../../base/common/history.js'; import { Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { ResourceSet } from '../../../../base/common/map.js'; @@ -76,10 +76,9 @@ import { IChatFollowup } from '../common/chatService.js'; import { IChatVariablesService } from '../common/chatVariables.js'; import { IChatResponseViewModel } from '../common/chatViewModel.js'; import { ChatInputHistoryMaxEntries, IChatHistoryEntry, IChatInputState, IChatWidgetHistoryService } from '../common/chatWidgetHistoryService.js'; -import { ChatAgentLocation, ChatConfiguration, ChatMode, validateChatMode } from '../common/constants.js'; +import { ChatAgentLocation, ChatConfiguration, ChatMode, isChatMode, validateChatMode } from '../common/constants.js'; import { ILanguageModelChatMetadata, ILanguageModelChatMetadataAndIdentifier, ILanguageModelsService } from '../common/languageModels.js'; import { CancelAction, ChatEditingSessionSubmitAction, ChatOpenModelPickerActionId, ChatSubmitAction, IChatExecuteActionContext, ToggleAgentModeActionId } from './actions/chatExecuteActions.js'; -import { AttachToolsAction } from './actions/chatToolActions.js'; import { ImplicitContextAttachmentWidget } from './attachments/implicitContextAttachment.js'; import { PromptInstructionsAttachmentsCollectionWidget } from './attachments/promptInstructions/promptInstructionsCollectionWidget.js'; import { IChatWidget } from './chat.js'; @@ -130,27 +129,29 @@ export interface IWorkingSetEntry { uri: URI; } +const GlobalLastChatModeKey = 'chat.lastChatMode'; + export class ChatInputPart extends Disposable implements IHistoryNavigationWidget { static readonly INPUT_SCHEME = 'chatSessionInput'; private static _counter = 0; - private _onDidLoadInputState = this._register(new Emitter()); - readonly onDidLoadInputState = this._onDidLoadInputState.event; + private _onDidLoadInputState: Emitter; + readonly onDidLoadInputState: Event; - private _onDidChangeHeight = this._register(new Emitter()); - readonly onDidChangeHeight = this._onDidChangeHeight.event; + private _onDidChangeHeight: Emitter; + readonly onDidChangeHeight: Event; - private _onDidFocus = this._register(new Emitter()); - readonly onDidFocus = this._onDidFocus.event; + private _onDidFocus: Emitter; + readonly onDidFocus: Event; - private _onDidBlur = this._register(new Emitter()); - readonly onDidBlur = this._onDidBlur.event; + private _onDidBlur: Emitter; + readonly onDidBlur: Event; - private _onDidChangeContext = this._register(new Emitter<{ removed?: IChatRequestVariableEntry[]; added?: IChatRequestVariableEntry[] }>()); - readonly onDidChangeContext = this._onDidChangeContext.event; + private _onDidChangeContext: Emitter<{ removed?: IChatRequestVariableEntry[]; added?: IChatRequestVariableEntry[] }>; + readonly onDidChangeContext: Event<{ removed?: IChatRequestVariableEntry[]; added?: IChatRequestVariableEntry[] }>; - private _onDidAcceptFollowup = this._register(new Emitter<{ followup: IChatFollowup; response: IChatResponseViewModel | undefined }>()); - readonly onDidAcceptFollowup = this._onDidAcceptFollowup.event; + private _onDidAcceptFollowup: Emitter<{ followup: IChatFollowup; response: IChatResponseViewModel | undefined }>; + readonly onDidAcceptFollowup: Event<{ followup: IChatFollowup; response: IChatResponseViewModel | undefined }>; private readonly _attachmentModel: ChatAttachmentModel; public get attachmentModel(): ChatAttachmentModel { @@ -215,7 +216,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge return (this.implicitContext.isPromptFile && this.implicitContext.enabled); } - private _indexOfLastAttachedContextDeletedWithKeyboard: number = -1; + private _indexOfLastAttachedContextDeletedWithKeyboard: number; private _implicitContext: ChatImplicitContext | undefined; public get implicitContext(): ChatImplicitContext | undefined { @@ -229,38 +230,38 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private _hasFileAttachmentContextKey: IContextKey; - private readonly _onDidChangeVisibility = this._register(new Emitter()); - private readonly _contextResourceLabels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this._onDidChangeVisibility.event }); + private readonly _onDidChangeVisibility: Emitter; + private readonly _contextResourceLabels: ResourceLabels; private readonly inputEditorMaxHeight: number; - private inputEditorHeight = 0; + private inputEditorHeight: number; private container!: HTMLElement; private inputSideToolbarContainer?: HTMLElement; private followupsContainer!: HTMLElement; - private readonly followupsDisposables = this._register(new DisposableStore()); + private readonly followupsDisposables: DisposableStore; private attachmentsContainer!: HTMLElement; private attachedContextContainer!: HTMLElement; - private readonly attachedContextDisposables = this._register(new MutableDisposable()); + private readonly attachedContextDisposables: MutableDisposable; private relatedFilesContainer!: HTMLElement; private chatEditingSessionWidgetContainer!: HTMLElement; - private _inputPartHeight: number = 0; + private _inputPartHeight: number; get inputPartHeight() { return this._inputPartHeight; } - private _followupsHeight: number = 0; + private _followupsHeight: number; get followupsHeight() { return this._followupsHeight; } - private _editSessionWidgetHeight: number = 0; + private _editSessionWidgetHeight: number; get editSessionWidgetHeight() { return this._editSessionWidgetHeight; } @@ -297,8 +298,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private chatMode: IContextKey; private modelWidget: ModelPickerActionItem | undefined; - private readonly _waitForPersistedLanguageModel = this._register(new MutableDisposable()); - private _onDidChangeCurrentLanguageModel = this._register(new Emitter()); + private readonly _waitForPersistedLanguageModel: MutableDisposable; + private _onDidChangeCurrentLanguageModel: Emitter; private _currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined; get currentLanguageModel() { @@ -309,10 +310,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge return this._currentLanguageModel; } - private _onDidChangeCurrentChatMode = this._register(new Emitter()); - readonly onDidChangeCurrentChatMode = this._onDidChangeCurrentChatMode.event; + private _onDidChangeCurrentChatMode: Emitter; + readonly onDidChangeCurrentChatMode: Event; - private _currentMode: ChatMode = ChatMode.Ask; + private _currentMode: ChatMode; public get currentMode(): ChatMode { return this._currentMode === ChatMode.Agent && !this.agentService.hasToolsAgent ? ChatMode.Edit : @@ -323,10 +324,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private cachedExecuteToolbarWidth: number | undefined; private cachedInputToolbarWidth: number | undefined; - readonly inputUri = URI.parse(`${ChatInputPart.INPUT_SCHEME}:input-${ChatInputPart._counter++}`); + readonly inputUri: URI; - private readonly _chatEditsActionsDisposables = this._register(new DisposableStore()); - private readonly _chatEditsDisposables = this._register(new DisposableStore()); + private readonly _chatEditsActionsDisposables: DisposableStore; + private readonly _chatEditsDisposables: DisposableStore; private _chatEditsListPool: CollapsibleListPool; private _chatEditList: IDisposableReference> | undefined; get selectedElements(): URI[] { @@ -341,7 +342,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge return edits; } - private _attemptedWorkingSetEntriesCount: number = 0; + private _attemptedWorkingSetEntriesCount: number; /** * The number of working set entries that the user actually wanted to attach. * This is less than or equal to {@link ChatInputPart.chatEditWorkingSetFiles}. @@ -385,6 +386,36 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge @IWorkbenchAssignmentService private readonly experimentService: IWorkbenchAssignmentService, ) { super(); + this._onDidLoadInputState = this._register(new Emitter()); + this.onDidLoadInputState = this._onDidLoadInputState.event; + this._onDidChangeHeight = this._register(new Emitter()); + this.onDidChangeHeight = this._onDidChangeHeight.event; + this._onDidFocus = this._register(new Emitter()); + this.onDidFocus = this._onDidFocus.event; + this._onDidBlur = this._register(new Emitter()); + this.onDidBlur = this._onDidBlur.event; + this._onDidChangeContext = this._register(new Emitter<{ removed?: IChatRequestVariableEntry[]; added?: IChatRequestVariableEntry[] }>()); + this.onDidChangeContext = this._onDidChangeContext.event; + this._onDidAcceptFollowup = this._register(new Emitter<{ followup: IChatFollowup; response: IChatResponseViewModel | undefined }>()); + this.onDidAcceptFollowup = this._onDidAcceptFollowup.event; + this._indexOfLastAttachedContextDeletedWithKeyboard = -1; + this._onDidChangeVisibility = this._register(new Emitter()); + this._contextResourceLabels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this._onDidChangeVisibility.event }); + this.inputEditorHeight = 0; + this.followupsDisposables = this._register(new DisposableStore()); + this.attachedContextDisposables = this._register(new MutableDisposable()); + this._inputPartHeight = 0; + this._followupsHeight = 0; + this._editSessionWidgetHeight = 0; + this._waitForPersistedLanguageModel = this._register(new MutableDisposable()); + this._onDidChangeCurrentLanguageModel = this._register(new Emitter()); + this._onDidChangeCurrentChatMode = this._register(new Emitter()); + this.onDidChangeCurrentChatMode = this._onDidChangeCurrentChatMode.event; + this._currentMode = ChatMode.Ask; + this.inputUri = URI.parse(`${ChatInputPart.INPUT_SCHEME}:input-${ChatInputPart._counter++}`); + this._chatEditsActionsDisposables = this._register(new DisposableStore()); + this._chatEditsDisposables = this._register(new DisposableStore()); + this._attemptedWorkingSetEntriesCount = 0; this._attachmentModel = this._register(this.instantiationService.createInstance(ChatAttachmentModel)); this.selectedToolsModel = this._register(this.instantiationService.createInstance(ChatSelectedTools, observableFromEvent(this, this.onDidChangeCurrentChatMode, () => this.currentMode))); @@ -517,7 +548,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } } - setChatMode(mode: ChatMode): void { + setChatMode(mode: ChatMode, storeSelection = true): void { if (!this.options.supportsChangingModes) { return; } @@ -526,6 +557,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this._currentMode = mode; this.chatMode.set(mode); this._onDidChangeCurrentChatMode.fire(); + + if (storeSelection) { + this.storageService.store(GlobalLastChatModeKey, mode, StorageScope.APPLICATION, StorageTarget.USER); + } } private modelSupportedForDefaultAgent(model: ILanguageModelChatMetadataAndIdentifier): boolean { @@ -614,6 +649,11 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge if (state.inputState?.chatMode) { this.setChatMode(state.inputState.chatMode); + } else { + const persistedMode = this.storageService.get(GlobalLastChatModeKey, StorageScope.APPLICATION); + if (isChatMode(persistedMode)) { + this.setChatMode(persistedMode); + } } // TODO@roblourens This is for an experiment which will be obsolete in a month or two and can then be removed. @@ -630,7 +670,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const defaultMode = validateChatMode(defaultModeTreatment); if (defaultMode) { this.logService.trace(`Applying default mode from experiment: ${defaultMode}`); - this.setChatMode(defaultMode); + this.setChatMode(defaultMode, false); this.checkModelSupported(); } } @@ -1011,29 +1051,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge telemetrySource: this.options.menus.telemetrySource, menuOptions: { shouldForwardArgs: true }, hiddenItemStrategy: HiddenItemStrategy.Ignore, - hoverDelegate - })); - this.inputActionsToolbar.context = { widget } satisfies IChatExecuteActionContext; - this._register(this.inputActionsToolbar.onDidChangeMenuItems(() => { - if (this.cachedDimensions && typeof this.cachedInputToolbarWidth === 'number' && this.cachedInputToolbarWidth !== this.inputActionsToolbar.getItemsWidth()) { - this.layout(this.cachedDimensions.height, this.cachedDimensions.width); - } - })); - this.executeToolbar = this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, toolbarsContainer, this.options.menus.executeToolbar, { - telemetrySource: this.options.menus.telemetrySource, - menuOptions: { - shouldForwardArgs: true - }, hoverDelegate, - hiddenItemStrategy: HiddenItemStrategy.Ignore, // keep it lean when hiding items and avoid a "..." overflow menu actionViewItemProvider: (action, options) => { - if (this.location === ChatAgentLocation.Panel || this.location === ChatAgentLocation.Editor) { - if ((action.id === ChatSubmitAction.ID || action.id === CancelAction.ID || action.id === ChatEditingSessionSubmitAction.ID) && action instanceof MenuItemAction) { - const dropdownAction = this.instantiationService.createInstance(MenuItemAction, { id: 'chat.moreExecuteActions', title: localize('notebook.moreExecuteActionsLabel', "More..."), icon: Codicon.chevronDown }, undefined, undefined, undefined, undefined); - return this.instantiationService.createInstance(ChatSubmitDropdownActionItem, action, dropdownAction, { ...options, menuAsChild: false }); - } - } - if (action.id === ChatOpenModelPickerActionId && action instanceof MenuItemAction) { if (!this._currentLanguageModel) { this.setCurrentLanguageModelToDefault(); @@ -1064,6 +1083,31 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge return undefined; } })); + this.inputActionsToolbar.getElement().classList.add('chat-input-toolbar'); + this.inputActionsToolbar.context = { widget } satisfies IChatExecuteActionContext; + this._register(this.inputActionsToolbar.onDidChangeMenuItems(() => { + if (this.cachedDimensions && typeof this.cachedInputToolbarWidth === 'number' && this.cachedInputToolbarWidth !== this.inputActionsToolbar.getItemsWidth()) { + this.layout(this.cachedDimensions.height, this.cachedDimensions.width); + } + })); + this.executeToolbar = this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, toolbarsContainer, this.options.menus.executeToolbar, { + telemetrySource: this.options.menus.telemetrySource, + menuOptions: { + shouldForwardArgs: true + }, + hoverDelegate, + hiddenItemStrategy: HiddenItemStrategy.Ignore, // keep it lean when hiding items and avoid a "..." overflow menu + actionViewItemProvider: (action, options) => { + if (this.location === ChatAgentLocation.Panel || this.location === ChatAgentLocation.Editor) { + if ((action.id === ChatSubmitAction.ID || action.id === CancelAction.ID || action.id === ChatEditingSessionSubmitAction.ID) && action instanceof MenuItemAction) { + const dropdownAction = this.instantiationService.createInstance(MenuItemAction, { id: 'chat.moreExecuteActions', title: localize('notebook.moreExecuteActionsLabel', "More..."), icon: Codicon.chevronDown }, undefined, undefined, undefined, undefined); + return this.instantiationService.createInstance(ChatSubmitDropdownActionItem, action, dropdownAction, { ...options, menuAsChild: false }); + } + } + + return undefined; + } + })); this.executeToolbar.getElement().classList.add('chat-execute-toolbar'); this.executeToolbar.context = { widget } satisfies IChatExecuteActionContext; this._register(this.executeToolbar.onDidChangeMenuItems(() => { @@ -1142,10 +1186,6 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const viewItem = this.instantiationService.createInstance(AddFilesButton, undefined, action, options); return viewItem; } - if (action.id === AttachToolsAction.id) { - // TODO@jrieken let's remove this once the tools picker has its final place. - return this.selectedToolsModel.toolsActionItemViewItemProvider(action, options); - } return undefined; } })); @@ -1155,8 +1195,6 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this._onDidChangeHeight.fire(); } })); - - this._register(this.selectedToolsModel.toolsActionItemViewItemProvider.onDidRender(() => this._onDidChangeHeight.fire())); } private renderAttachedContext() { diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index 1d11f67b0ab..053797cfae4 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -13,6 +13,7 @@ import { IListVirtualDelegate } from '../../../../base/browser/ui/list/list.js'; import { ITreeNode, ITreeRenderer } from '../../../../base/browser/ui/tree/tree.js'; import { IAction } from '../../../../base/common/actions.js'; import { coalesce, distinct } from '../../../../base/common/arrays.js'; +import { findLast } from '../../../../base/common/arraysFind.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { FuzzyScore } from '../../../../base/common/filters.js'; @@ -23,7 +24,6 @@ import { Disposable, DisposableStore, IDisposable, dispose, thenIfNotDisposed, t import { ResourceMap } from '../../../../base/common/map.js'; import { FileAccess } from '../../../../base/common/network.js'; import { clamp } from '../../../../base/common/numbers.js'; -import { autorun } from '../../../../base/common/observable.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { URI } from '../../../../base/common/uri.js'; import { MarkdownRenderer } from '../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js'; @@ -48,7 +48,7 @@ import { IChatAgentMetadata } from '../common/chatAgents.js'; import { ChatContextKeys } from '../common/chatContextKeys.js'; import { IChatRequestVariableEntry, IChatTextEditGroup } from '../common/chatModel.js'; import { chatSubcommandLeader } from '../common/chatParserTypes.js'; -import { ChatAgentVoteDirection, ChatAgentVoteDownReason, ChatErrorLevel, IChatConfirmation, IChatContentReference, IChatExtensionsContent, IChatFollowup, IChatMarkdownContent, IChatTask, IChatToolInvocation, IChatToolInvocationSerialized, IChatTreeData, IChatUndoStop } from '../common/chatService.js'; +import { ChatAgentVoteDirection, ChatAgentVoteDownReason, ChatErrorLevel, IChatConfirmation, IChatContentReference, IChatExtensionsContent, IChatFollowup, IChatMarkdownContent, IChatTask, IChatTaskSerialized, IChatToolInvocation, IChatToolInvocationSerialized, IChatTreeData, IChatUndoStop } from '../common/chatService.js'; import { IChatCodeCitations, IChatReferences, IChatRendererContent, IChatRequestViewModel, IChatResponseViewModel, IChatWorkingProgress, isRequestVM, isResponseVM } from '../common/chatViewModel.js'; import { getNWords } from '../common/chatWordCounter.js'; import { CodeBlockModelCollection } from '../common/codeBlockModelCollection.js'; @@ -69,9 +69,9 @@ import { ChatQuotaExceededPart } from './chatContentParts/chatQuotaExceededPart. import { ChatCollapsibleListContentPart, ChatUsedReferencesListContentPart, CollapsibleListPool } from './chatContentParts/chatReferencesContentPart.js'; import { ChatTaskContentPart } from './chatContentParts/chatTaskContentPart.js'; import { ChatTextEditContentPart, DiffEditorPool } from './chatContentParts/chatTextEditContentPart.js'; -import { ChatToolInvocationPart } from './chatContentParts/chatToolInvocationPart.js'; import { ChatTreeContentPart, TreePool } from './chatContentParts/chatTreeContentPart.js'; import { ChatWarningContentPart } from './chatContentParts/chatWarningContentPart.js'; +import { ChatToolInvocationPart } from './chatContentParts/toolInvocationParts/chatToolInvocationPart.js'; import { ChatMarkdownDecorationsRenderer } from './chatMarkdownDecorationsRenderer.js'; import { ChatMarkdownRenderer } from './chatMarkdownRenderer.js'; import { ChatEditorOptions } from './chatOptions.js'; @@ -81,6 +81,10 @@ const $ = dom.$; interface IChatListItemTemplate { currentElement?: ChatTreeItem; + /** + * The parts that are currently rendered in the template. Note that these are purposely not added to elementDisposables- + * they are disposed in a separate cycle after diffing with the next content to render. + */ renderedParts?: IChatContentPart[]; readonly rowContainer: HTMLElement; readonly titleToolbar?: MenuWorkbenchToolBar; @@ -448,21 +452,14 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { - this._renderDetail(element, templateData); - })); - } - - private _renderDetail(element: IChatResponseViewModel, templateData: IChatListItemTemplate): void { - dom.clearNode(templateData.detail); if (element.agentOrSlashCommandDetected) { @@ -520,99 +517,32 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer templateData.value.classList.remove('inline-progress'))); - value.push({ content: new MarkdownString('', { supportHtml: true }), kind: 'markdownContent' }); - } else { - templateData.value.classList.remove('inline-progress'); - } - - } else if (isResponseVM(element)) { - if (element.contentReferences.length) { - value.push({ kind: 'references', references: element.contentReferences }); - } - value.push(...annotateSpecialMarkdownContent(element.response.value)); - if (element.codeCitations.length) { - value.push({ kind: 'codeCitations', citations: element.codeCitations }); - } + const content: IChatRendererContent[] = []; + // Always add the references to avoid shifting the content parts when a reference is added, and having to re-diff all the content. + // The part will hide itself if the list is empty. + content.push({ kind: 'references', references: element.contentReferences }); + content.push(...annotateSpecialMarkdownContent(element.response.value)); + if (element.codeCitations.length) { + content.push({ kind: 'codeCitations', citations: element.codeCitations }); } - dom.clearNode(templateData.value); - - if (isResponseVM(element)) { - this.renderDetail(element, templateData); - } - - const isFiltered = !!(isResponseVM(element) && element.errorDetails?.responseIsFiltered); - - const parts: IChatContentPart[] = []; + const isFiltered = !!element.errorDetails?.responseIsFiltered; if (!isFiltered) { - - let inlineSlashCommandRendered = false; - - value.forEach((data, index) => { - const context: IChatContentPartRenderContext = { - element, - contentIndex: index, - content: value, - preceedingContentParts: parts, - container: templateData.rowContainer, - }; - const newPart = this.renderChatContentPart(data, templateData, context); - if (newPart) { - - if (this.rendererOptions.renderDetectedCommandsWithRequest - && !inlineSlashCommandRendered - && isRequestVM(element) && element.agentOrSlashCommandDetected && element.slashCommand - && data.kind === 'markdownContent' // TODO this is fishy but I didn't find a better way to render on the same inline as the MD request part - ) { - if (newPart.domNode) { - newPart.domNode.style.display = 'inline-flex'; - } - const cmdPart = this.instantiationService.createInstance(ChatAgentCommandContentPart, element.slashCommand, () => this._onDidClickRerunWithAgentOrCommandDetection.fire({ sessionId: element.sessionId, requestId: element.id })); - templateData.value.appendChild(cmdPart.domNode); - parts.push(cmdPart); - inlineSlashCommandRendered = true; - } - - if (newPart.domNode) { - templateData.value.appendChild(newPart.domNode); - } - parts.push(newPart); - } - }); - } - - if (templateData.renderedParts) { - dispose(templateData.renderedParts); - } - templateData.renderedParts = parts; - - if (!isFiltered) { - if (isRequestVM(element) && element.variables.length) { - const newPart = this.renderAttachments(element.variables, element.contentReferences, templateData); - if (newPart) { - if (newPart.domNode) { - // p has a :last-child rule for margin - templateData.value.appendChild(newPart.domNode); - } - templateData.elementDisposables.add(newPart); - } + const diff = this.diff(templateData.renderedParts ?? [], content, element); + this.renderChatContentDiff(diff, content, element, templateData); + } else { + dom.clearNode(templateData.value); + if (templateData.renderedParts) { + dispose(templateData.renderedParts); } + templateData.renderedParts = []; } - if (isResponseVM(element) && element.errorDetails?.message) { + + if (element.errorDetails?.message) { if (element.errorDetails.isQuotaExceeded) { const renderedError = this.instantiationService.createInstance(ChatQuotaExceededPart, element, this.renderer); templateData.elementDisposables.add(renderedError); @@ -626,6 +556,82 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer templateData.value.classList.remove('inline-progress'))); + content.push({ content: new MarkdownString('', { supportHtml: true }), kind: 'markdownContent' }); + } else { + templateData.value.classList.remove('inline-progress'); + } + } + + dom.clearNode(templateData.value); + const parts: IChatContentPart[] = []; + + let inlineSlashCommandRendered = false; + content.forEach((data, index) => { + const context: IChatContentPartRenderContext = { + element, + contentIndex: index, + content: content, + preceedingContentParts: parts, + container: templateData.rowContainer, + }; + const newPart = this.renderChatContentPart(data, templateData, context); + if (newPart) { + + if (this.rendererOptions.renderDetectedCommandsWithRequest + && !inlineSlashCommandRendered + && element.agentOrSlashCommandDetected && element.slashCommand + && data.kind === 'markdownContent' // TODO this is fishy but I didn't find a better way to render on the same inline as the MD request part + ) { + if (newPart.domNode) { + newPart.domNode.style.display = 'inline-flex'; + } + const cmdPart = this.instantiationService.createInstance(ChatAgentCommandContentPart, element.slashCommand, () => this._onDidClickRerunWithAgentOrCommandDetection.fire({ sessionId: element.sessionId, requestId: element.id })); + templateData.value.appendChild(cmdPart.domNode); + parts.push(cmdPart); + inlineSlashCommandRendered = true; + } + + if (newPart.domNode) { + templateData.value.appendChild(newPart.domNode); + } + parts.push(newPart); + } + }); + + if (templateData.renderedParts) { + dispose(templateData.renderedParts); + } + templateData.renderedParts = parts; + + if (element.variables.length) { + const newPart = this.renderAttachments(element.variables, element.contentReferences, templateData); + if (newPart.domNode) { + // p has a :last-child rule for margin + templateData.value.appendChild(newPart.domNode); + } + templateData.elementDisposables.add(newPart); + } + + this.updateItemHeightOnRender(element, templateData); + } + + private updateItemHeightOnRender(element: ChatTreeItem, templateData: IChatListItemTemplate) { const newHeight = templateData.rowContainer.offsetHeight; const fireEvent = !element.currentRenderedHeight || element.currentRenderedHeight !== newHeight; element.currentRenderedHeight = newHeight; @@ -661,7 +667,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer part.kind !== 'markdownContent' || part.content.value.trim().length > 0); if ( !lastPart || lastPart.kind === 'references' || (lastPart.kind === 'toolInvocation' && (lastPart.isComplete || lastPart.presentation === 'hidden')) || ((lastPart.kind === 'textEditGroup' || lastPart.kind === 'notebookEditGroup') && lastPart.done && !partsToRender.some(part => part.kind === 'toolInvocation' && !part.isComplete)) || - (lastPart.kind === 'progressTask' && lastPart.deferred.isSettled) + (lastPart.kind === 'progressTask' && lastPart.deferred.isSettled) || + lastPart.kind === 'prepareToolInvocation' ) { return true; } @@ -888,7 +905,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer this.trustedDomainService.isValid(uri), + remoteImageIsAllowed: (_uri) => false, sanitizerOptions: { replaceWithPlaintext: true, allowedTags: allowedHtmlTags, diff --git a/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts index ed49ba1a4db..3271c01bfd2 100644 --- a/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts @@ -71,7 +71,10 @@ const chatViewDescriptor: IViewDescriptor[] = [{ ChatContextKeys.Setup.hidden.negate(), ChatContextKeys.Setup.disabled.negate() // do not pretend a working Chat view if extension is explicitly disabled ), - ChatContextKeys.Setup.installed, + ContextKeyExpr.and( + ChatContextKeys.Setup.installed, + ChatContextKeys.Setup.disabled.negate() // do not pretend a working Chat view if extension is explicitly disabled + ), ChatContextKeys.panelParticipantRegistered, ChatContextKeys.extensionInvalid ) diff --git a/src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts b/src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts index 72b81354148..ec874a2f0b3 100644 --- a/src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts +++ b/src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts @@ -66,16 +66,31 @@ class ChatResponseAccessibleProvider extends Disposable implements IAccessibleVi responseContent = item.errorDetails.message; } if (isResponseVM(item)) { - const toolInvocation = item.response.value.find(item => item.kind === 'toolInvocation'); - if (toolInvocation?.confirmationMessages) { - const title = toolInvocation.confirmationMessages.title; - const message = typeof toolInvocation.confirmationMessages.message === 'string' ? toolInvocation.confirmationMessages.message : stripIcons(renderMarkdownAsPlaintext(toolInvocation.confirmationMessages.message)); - const terminalCommand = toolInvocation.toolSpecificData && 'command' in toolInvocation.toolSpecificData ? toolInvocation.toolSpecificData.command : undefined; - responseContent += `${title}`; - if (terminalCommand) { - responseContent += `: ${terminalCommand}`; + + const toolInvocations = item.response.value.filter(item => item.kind === 'toolInvocation'); + for (const toolInvocation of toolInvocations) { + if (toolInvocation.confirmationMessages) { + const title = toolInvocation.confirmationMessages.title; + const message = typeof toolInvocation.confirmationMessages.message === 'string' ? toolInvocation.confirmationMessages.message : stripIcons(renderMarkdownAsPlaintext(toolInvocation.confirmationMessages.message)); + const command = toolInvocation.toolSpecificData && 'command' in toolInvocation.toolSpecificData ? toolInvocation.toolSpecificData.command : undefined; + responseContent += `${title}`; + if (command) { + responseContent += `: ${command}`; + } + responseContent += `\n${message}\n`; + } else if (toolInvocation.isComplete && toolInvocation.resultDetails && 'input' in toolInvocation.resultDetails) { + responseContent += toolInvocation.resultDetails.isError ? 'Errored ' : 'Completed '; + responseContent += `${`${typeof toolInvocation.invocationMessage === 'string' ? toolInvocation.invocationMessage : stripIcons(renderMarkdownAsPlaintext(toolInvocation.invocationMessage))} with input: ${toolInvocation.resultDetails.input}`}\n`; + } + } + + const pastConfirmations = item.response.value.filter(item => item.kind === 'toolInvocationSerialized'); + for (const pastConfirmation of pastConfirmations) { + if (pastConfirmation.isComplete && pastConfirmation.resultDetails && 'input' in pastConfirmation.resultDetails) { + if (pastConfirmation.pastTenseMessage) { + responseContent += `\n${`${typeof pastConfirmation.pastTenseMessage === 'string' ? pastConfirmation.pastTenseMessage : stripIcons(renderMarkdownAsPlaintext(pastConfirmation.pastTenseMessage))} with input: ${pastConfirmation.resultDetails.input}`}\n`; + } } - responseContent += `\n${message}`; } } return renderMarkdownAsPlaintext(new MarkdownString(responseContent), true); diff --git a/src/vs/workbench/contrib/chat/browser/chatSelectedTools.ts b/src/vs/workbench/contrib/chat/browser/chatSelectedTools.ts index 8564170f9b7..b068a640bc3 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSelectedTools.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSelectedTools.ts @@ -3,18 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { reset } from '../../../../base/browser/dom.js'; -import { IActionViewItemProvider } from '../../../../base/browser/ui/actionbar/actionbar.js'; -import { IActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js'; -import { renderLabelWithIcons } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; -import { IAction } from '../../../../base/common/actions.js'; -import { Emitter, Event } from '../../../../base/common/event.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; -import { autorun, derived, IObservable, observableFromEvent } from '../../../../base/common/observable.js'; -import { assertType } from '../../../../base/common/types.js'; -import { localize } from '../../../../nls.js'; -import { MenuEntryActionViewItem } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; -import { MenuItemAction } from '../../../../platform/actions/common/actions.js'; +import { derived, IObservable, observableFromEvent } from '../../../../base/common/observable.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ObservableMemento, observableMemento } from '../../../../platform/observable/common/observableMemento.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; @@ -41,8 +31,6 @@ export class ChatSelectedTools extends Disposable { readonly tools: IObservable; - readonly toolsActionItemViewItemProvider: IActionViewItemProvider & { onDidRender: Event }; - private readonly _allTools: IObservable[]>; constructor( @@ -77,54 +65,6 @@ export class ChatSelectedTools extends Disposable { !(disabled.toolIds.has(t.id) || disabled.buckets.has(ToolDataSource.toKey(t.source))) ); }); - - const toolsCount = derived(r => { - const count = this._allTools.read(r).length; - const enabled = this.tools.read(r).length; - return { count, enabled }; - }); - - const onDidRender = this._store.add(new Emitter()); - - this.toolsActionItemViewItemProvider = Object.assign( - (action: IAction, options: IActionViewItemOptions) => { - if (!(action instanceof MenuItemAction)) { - return undefined; - } - - return instaService.createInstance(class extends MenuEntryActionViewItem { - - override render(container: HTMLElement): void { - this.options.icon = false; - this.options.label = true; - container.classList.add('chat-mcp', 'chat-attachment-button'); - super.render(container); - } - - protected override updateLabel(): void { - this._store.add(autorun(r => { - assertType(this.label); - - const { enabled, count } = toolsCount.read(r); - - const message = count === 0 - ? '$(tools)' - : enabled !== count - ? localize('tool.1', "{0} {1} of {2}", '$(tools)', enabled, count) - : localize('tool.0', "{0} {1}", '$(tools)', count); - - reset(this.label, ...renderLabelWithIcons(message)); - - if (this.element?.isConnected) { - onDidRender.fire(); - } - })); - } - - }, action, { ...options, keybindingNotRenderedWithLabel: true }); - }, - { onDidRender: onDidRender.event } - ); } selectOnly(toolIds: readonly string[]): void { @@ -146,7 +86,7 @@ export class ChatSelectedTools extends Disposable { const result = new Map(); const enabledTools = new Set(this.tools.get().map(t => t.id)); for (const tool of this._allTools.get()) { - if (tool.supportsToolPicker) { + if (tool.canBeReferencedInPrompt) { result.set(tool, enabledTools.has(tool.id)); } } diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.ts index de32b19d4a4..840c7ecf8e2 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -5,8 +5,8 @@ import './media/chatSetup.css'; import { $ } from '../../../../base/browser/dom.js'; -import { Dialog } from '../../../../base/browser/ui/dialog/dialog.js'; -import { toAction, WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from '../../../../base/common/actions.js'; +import { Dialog, DialogContentsAlignment } from '../../../../base/browser/ui/dialog/dialog.js'; +import { WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from '../../../../base/common/actions.js'; import { timeout } from '../../../../base/common/async.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { Codicon } from '../../../../base/common/codicons.js'; @@ -29,7 +29,6 @@ import { ICommandService } from '../../../../platform/commands/common/commands.j import { ConfigurationTarget, IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { Extensions as ConfigurationExtensions, IConfigurationRegistry } from '../../../../platform/configuration/common/configurationRegistry.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; -import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; import { createWorkbenchDialogOptions } from '../../../../platform/dialogs/browser/dialog.js'; import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; @@ -67,6 +66,7 @@ import { ILanguageModelsService } from '../common/languageModels.js'; import { CHAT_CATEGORY, CHAT_OPEN_ACTION_ID, CHAT_SETUP_ACTION_ID } from './actions/chatActions.js'; import { ChatViewId, IChatWidgetService, showCopilotView } from './chat.js'; import { CHAT_SIDEBAR_PANEL_ID } from './chatViewPane.js'; +import { coalesce } from '../../../../base/common/arrays.js'; const defaultChat = { extensionId: product.defaultChatAgent?.extensionId ?? '', @@ -79,6 +79,8 @@ const defaultChat = { providerName: product.defaultChatAgent?.providerName ?? '', enterpriseProviderId: product.defaultChatAgent?.enterpriseProviderId ?? '', enterpriseProviderName: product.defaultChatAgent?.enterpriseProviderName ?? '', + alternativeProviderId: product.defaultChatAgent?.alternativeProviderId ?? '', + alternativeProviderName: product.defaultChatAgent?.alternativeProviderName ?? '', providerUriSetting: product.defaultChatAgent?.providerUriSetting ?? '', providerScopes: product.defaultChatAgent?.providerScopes ?? [[]], manageSettingsUrl: product.defaultChatAgent?.manageSettingsUrl ?? '', @@ -152,7 +154,6 @@ class SetupAgent extends Disposable implements IChatAgentImplementation { canBeReferencedInPrompt: true, toolReferenceName: 'new', when: ContextKeyExpr.true(), - supportsToolPicker: true, }).disposable); return { agent, disposable: disposables }; @@ -180,6 +181,9 @@ class SetupAgent extends Disposable implements IChatAgentImplementation { const agent = disposables.add(instantiationService.createInstance(SetupAgent, context, controller, location)); disposables.add(chatAgentService.registerAgentImplementation(id, agent)); + if (mode === ChatMode.Agent) { + chatAgentService.updateAgent(id, { themeIcon: Codicon.tools }); + } return { agent, disposable: disposables }; } @@ -216,7 +220,7 @@ class SetupAgent extends Disposable implements IChatAgentImplementation { } private async doInvoke(request: IChatAgentRequest, progress: (part: IChatProgress) => void, chatService: IChatService, languageModelsService: ILanguageModelsService, chatWidgetService: IChatWidgetService, chatAgentService: IChatAgentService, languageModelToolsService: ILanguageModelToolsService): Promise { - if (!this.context.state.installed || this.context.state.entitlement === ChatEntitlement.Available || this.context.state.entitlement === ChatEntitlement.Unknown) { + if (!this.context.state.installed || this.context.state.disabled || this.context.state.entitlement === ChatEntitlement.Available || this.context.state.entitlement === ChatEntitlement.Unknown) { return this.doInvokeWithSetup(request, progress, chatService, languageModelsService, chatWidgetService, chatAgentService, languageModelToolsService); } @@ -573,7 +577,7 @@ class ChatSetup { let instance = ChatSetup.instance; if (!instance) { instance = ChatSetup.instance = instantiationService.invokeFunction(accessor => { - return new ChatSetup(context, controller, instantiationService, accessor.get(ITelemetryService), accessor.get(IContextMenuService), accessor.get(IWorkbenchLayoutService), accessor.get(IKeybindingService), accessor.get(IChatEntitlementService), accessor.get(ILogService), accessor.get(IConfigurationService)); + return new ChatSetup(context, controller, instantiationService, accessor.get(ITelemetryService), accessor.get(IWorkbenchLayoutService), accessor.get(IKeybindingService), accessor.get(IChatEntitlementService), accessor.get(ILogService), accessor.get(IConfigurationService)); }); } @@ -589,7 +593,6 @@ class ChatSetup { private readonly controller: Lazy, @IInstantiationService private readonly instantiationService: IInstantiationService, @ITelemetryService private readonly telemetryService: ITelemetryService, - @IContextMenuService private readonly contextMenuService: IContextMenuService, @ILayoutService private readonly layoutService: IWorkbenchLayoutService, @IKeybindingService private readonly keybindingService: IKeybindingService, @IChatEntitlementService private readonly chatEntitlementService: IChatEntitlementService, @@ -657,46 +660,48 @@ class ChatSetup { private async showDialog(): Promise { const disposables = new DisposableStore(); - let result: ChatSetupStrategy | undefined = undefined; - - const buttons = [this.getPrimaryButton(), localize('maybeLater', "Maybe Later")]; + const buttons = this.getButtons(); const dialog = disposables.add(new Dialog( this.layoutService.activeContainer, this.getDialogTitle(), - buttons, + buttons.map(button => button[0]), createWorkbenchDialogOptions({ type: 'none', icon: Codicon.copilotLarge, - cancelId: buttons.length - 1, - renderBody: body => body.appendChild(this.createDialog(disposables)), - primaryButtonDropdown: { - contextMenuProvider: this.contextMenuService, - addPrimaryActionToDropdown: false, - actions: [ - toAction({ id: 'setupWithProvider', label: localize('setupWithProvider', "Sign in with a {0} Account", defaultChat.providerName), run: () => result = ChatSetupStrategy.SetupWithoutEnterpriseProvider }), - toAction({ id: 'setupWithEnterpriseProvider', label: localize('setupWithEnterpriseProvider', "Sign in with a {0} Account", defaultChat.enterpriseProviderName), run: () => result = ChatSetupStrategy.SetupWithEnterpriseProvider }), - ] - } + alignment: DialogContentsAlignment.Vertical, + cancelId: -1, // not offered as button, but X can cancel + renderFooter: this.telemetryService.telemetryLevel !== TelemetryLevel.NONE ? footer => footer.appendChild(this.createDialogFooter(disposables)) : undefined, + buttonOptions: buttons.map(button => button[2]) }, this.keybindingService, this.layoutService) )); const { button } = await dialog.show(); disposables.dispose(); - return button === 0 ? result ?? ChatSetupStrategy.DefaultSetup : ChatSetupStrategy.Canceled; + return buttons[button]?.[1] ?? ChatSetupStrategy.Canceled; } - private getPrimaryButton(): string { + private getButtons(): Array<[string, ChatSetupStrategy, { renderAsLink: boolean } | undefined]> { if (this.context.state.entitlement === ChatEntitlement.Unknown) { + const supportAlternateProvider = this.configurationService.getValue('chat.setup.signInWithAlternateProvider') === true && defaultChat.alternativeProviderId; + if (ChatEntitlementRequests.providerId(this.configurationService) === defaultChat.enterpriseProviderId) { - return localize('setupWithProviderShort', "Sign in with {0}", defaultChat.enterpriseProviderName); + return coalesce([ + [localize('continueWithProvider', "Continue with {0}", defaultChat.enterpriseProviderName), ChatSetupStrategy.SetupWithEnterpriseProvider, undefined], + supportAlternateProvider ? [localize('continueWithProvider', "Continue with {0}", defaultChat.alternativeProviderName), ChatSetupStrategy.SetupWithoutEnterpriseProvider, undefined] : undefined, + [localize('signInWithProvider', "Sign in with a {0} account", defaultChat.providerName), ChatSetupStrategy.SetupWithoutEnterpriseProvider, { renderAsLink: true }] + ]); } - return localize('signInButton', "Sign in"); + return coalesce([ + [localize('continueWithProvider', "Continue with {0}", defaultChat.providerName), ChatSetupStrategy.SetupWithoutEnterpriseProvider, undefined], + supportAlternateProvider ? [localize('continueWithProvider', "Continue with {0}", defaultChat.alternativeProviderName), ChatSetupStrategy.SetupWithoutEnterpriseProvider, undefined] : undefined, + [localize('signInWithProvider', "Sign in with a {0} account", defaultChat.enterpriseProviderName), ChatSetupStrategy.SetupWithEnterpriseProvider, { renderAsLink: true }] + ]); } - return localize('useCopilotButton', "Use Copilot"); + return [[localize('setupCopilotButton', "Set up Copilot"), ChatSetupStrategy.DefaultSetup, undefined]]; } private getDialogTitle(): string { @@ -711,20 +716,14 @@ class ChatSetup { return this.context.state.registered ? localize('copilotTitle', "Start using Copilot") : localize('copilotFreeTitle', "Start using Copilot for free"); } - private createDialog(disposables: DisposableStore): HTMLElement { - const element = $('.chat-setup-dialog'); + private createDialogFooter(disposables: DisposableStore): HTMLElement { + const element = $('.chat-setup-dialog-footer'); const markdown = this.instantiationService.createInstance(MarkdownRenderer, {}); - // Header - const header = localize({ key: 'headerDialog', comment: ['{Locked="[Copilot]({0})"}'] }, "[Copilot]({0}) is your AI pair programmer. Write code faster with completions, fix bugs and build new features across multiple files, and learn about your codebase through chat.", defaultChat.documentationUrl); - element.appendChild($('p.setup-header', undefined, disposables.add(markdown.render(new MarkdownString(header, { isTrusted: true }))).element)); - // SKU Settings - if (this.telemetryService.telemetryLevel !== TelemetryLevel.NONE) { - const settings = localize({ key: 'settings', comment: ['{Locked="["}', '{Locked="]({0})"}', '{Locked="]({1})"}'] }, "GitHub Copilot Free, Pro and Pro+ may show [public code]({0}) suggestions and we may use your data for product improvement. You can change these [settings]({1}) at any time.", defaultChat.publicCodeMatchesUrl, defaultChat.manageSettingsUrl); - element.appendChild($('p.setup-settings', undefined, disposables.add(markdown.render(new MarkdownString(settings, { isTrusted: true }))).element)); - } + const settings = localize({ key: 'settings', comment: ['{Locked="["}', '{Locked="]({0})"}', '{Locked="]({1})"}'] }, "GitHub Copilot Free, Pro and Pro+ may show [public code]({0}) suggestions and we may use your data for product improvement. You can change these [settings]({1}) at any time.", defaultChat.publicCodeMatchesUrl, defaultChat.manageSettingsUrl); + element.appendChild($('p.setup-settings', undefined, disposables.add(markdown.render(new MarkdownString(settings, { isTrusted: true }))).element)); return element; } @@ -762,8 +761,7 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr const vscodeAgentDisposables = markAsSingleton(new MutableDisposable()); const updateRegistration = () => { - const disabled = context.state.hidden /* via "Hide Copilot" */ || context.state.disabled /* via extension enablement */; - if (!disabled) { + if (!context.state.hidden && !context.state.disabled) { // Default Agents (always, even if installed to allow for speedy requests right on startup) if (!defaultAgentDisposables.value) { @@ -791,8 +789,8 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr disposables.add(SetupAgent.registerDefaultAgents(this.instantiationService, ChatAgentLocation.Editor, undefined, context, controller).disposable); } - // VSCode Agent + Tool (unless installed) - if (!context.state.installed && !vscodeAgentDisposables.value) { + // VSCode Agent + Tool (unless installed and enabled) + if (!(context.state.installed && !context.state.disabled) && !vscodeAgentDisposables.value) { const disposables = vscodeAgentDisposables.value = new DisposableStore(); disposables.add(SetupAgent.registerVSCodeAgent(this.instantiationService, context, controller).disposable); @@ -802,7 +800,7 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr vscodeAgentDisposables.clear(); } - if (context.state.installed) { + if (context.state.installed && !context.state.disabled) { vscodeAgentDisposables.clear(); // we need to do this to prevent showing duplicate agent/tool entries in the list } }; @@ -830,7 +828,7 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr }); } - override async run(accessor: ServicesAccessor, mode: ChatMode): Promise { + override async run(accessor: ServicesAccessor, mode: ChatMode): Promise { const viewsService = accessor.get(IViewsService); const layoutService = accessor.get(IWorkbenchLayoutService); const instantiationService = accessor.get(IInstantiationService); @@ -856,9 +854,11 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr }); if (confirmed) { - commandService.executeCommand(CHAT_SETUP_ACTION_ID); + return Boolean(await commandService.executeCommand(CHAT_SETUP_ACTION_ID)); } } + + return Boolean(success); } } @@ -1227,7 +1227,7 @@ class ChatSetupController extends Disposable { } private async install(session: AuthenticationSession | undefined, entitlement: ChatEntitlement, providerId: string, watch: StopWatch): Promise { - const wasInstalled = this.context.state.installed; + const wasRunning = this.context.state.installed && !this.context.state.disabled; let signUpResult: boolean | { errorCode: number } | undefined = undefined; try { @@ -1265,10 +1265,10 @@ class ChatSetupController extends Disposable { } if (typeof signUpResult === 'boolean') { - this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: wasInstalled && !signUpResult ? 'alreadyInstalled' : 'installed', installDuration: watch.elapsed(), signUpErrorCode: undefined }); + this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: wasRunning && !signUpResult ? 'alreadyInstalled' : 'installed', installDuration: watch.elapsed(), signUpErrorCode: undefined }); } - if (wasInstalled && signUpResult === true) { + if (wasRunning && signUpResult === true) { refreshTokens(this.commandService); } @@ -1350,7 +1350,6 @@ class ChatSetupController extends Disposable { ...existingAdvancedSetting, 'authProvider': undefined } : undefined, ConfigurationTarget.USER); - await this.configurationService.updateValue(defaultChat.providerUriSetting, undefined, ConfigurationTarget.USER); } return this.setup({ ...options, forceSignIn: true }); diff --git a/src/vs/workbench/contrib/chat/browser/chatStatus.ts b/src/vs/workbench/contrib/chat/browser/chatStatus.ts index a90219c023b..42b0cd50489 100644 --- a/src/vs/workbench/contrib/chat/browser/chatStatus.ts +++ b/src/vs/workbench/contrib/chat/browser/chatStatus.ts @@ -11,7 +11,7 @@ import { localize } from '../../../../nls.js'; import { IWorkbenchContribution } from '../../../common/contributions.js'; import { IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarService, ShowTooltipCommand, StatusbarAlignment, StatusbarEntryKind } from '../../../services/statusbar/browser/statusbar.js'; import { $, addDisposableListener, append, clearNode, EventHelper, EventType } from '../../../../base/browser/dom.js'; -import { ChatEntitlement, ChatEntitlementService, ChatSentiment, IChatEntitlementService, IQuotaSnapshot } from '../common/chatEntitlementService.js'; +import { ChatEntitlement, ChatEntitlementService, IChatEntitlementService, IQuotaSnapshot } from '../common/chatEntitlementService.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { defaultButtonStyles, defaultCheckboxStyles } from '../../../../platform/theme/browser/defaultStyles.js'; import { Checkbox } from '../../../../base/browser/ui/toggle/toggle.js'; @@ -126,7 +126,7 @@ export class ChatStatusBarEntry extends Disposable implements IWorkbenchContribu } private update(): void { - if (this.chatEntitlementService.sentiment !== ChatSentiment.Disabled) { + if (!this.chatEntitlementService.sentiment.hidden) { if (!this.entry) { this.entry = this.statusbarService.addEntry(this.getEntryProps(), 'chat.statusBarEntry', StatusbarAlignment.RIGHT, { location: { id: 'status.editor.mode', priority: 100.1 }, alignment: StatusbarAlignment.RIGHT }); } else { @@ -175,8 +175,14 @@ export class ChatStatusBarEntry extends Disposable implements IWorkbenchContribu const chatQuotaExceeded = this.chatEntitlementService.quotas.chat?.percentRemaining === 0; const completionsQuotaExceeded = this.chatEntitlementService.quotas.completions?.percentRemaining === 0; + // Disabled + if (this.chatEntitlementService.sentiment.disabled) { + text = `$(copilot-unavailable)`; + ariaLabel = localize('copilotDisabledStatus', "Copilot Disabled"); + } + // Signed out - if (this.chatEntitlementService.entitlement === ChatEntitlement.Unknown) { + else if (this.chatEntitlementService.entitlement === ChatEntitlement.Unknown) { const signedOutWarning = localize('notSignedIntoCopilot', "Signed out"); text = `$(copilot-not-connected) ${signedOutWarning}`; @@ -227,17 +233,18 @@ export class ChatStatusBarEntry extends Disposable implements IWorkbenchContribu } function isNewUser(chatEntitlementService: IChatEntitlementService): boolean { - return chatEntitlementService.sentiment !== ChatSentiment.Installed || // copilot not installed + return !chatEntitlementService.sentiment.installed || // copilot not installed chatEntitlementService.entitlement === ChatEntitlement.Available; // not yet signed up to copilot } function canUseCopilot(chatEntitlementService: IChatEntitlementService): boolean { const newUser = isNewUser(chatEntitlementService); + const disabled = chatEntitlementService.sentiment.disabled; const signedOut = chatEntitlementService.entitlement === ChatEntitlement.Unknown; const limited = chatEntitlementService.entitlement === ChatEntitlement.Limited; const allFreeQuotaReached = limited && chatEntitlementService.quotas.chat?.percentRemaining === 0 && chatEntitlementService.quotas.completions?.percentRemaining === 0; - return !newUser && !signedOut && !allFreeQuotaReached; + return !newUser && !signedOut && !allFreeQuotaReached && !disabled; } function isCompletionsEnabled(configurationService: IConfigurationService, modeId: string = '*'): boolean { @@ -327,9 +334,9 @@ class ChatStatusDashboard extends Disposable { run: () => this.runCommandAndClose(() => this.openerService.open(URI.parse(defaultChat.manageSettingsUrl))), })); - const completionsQuotaIndicator = completionsQuota ? this.createQuotaIndicator(this.element, disposables, completionsQuota, localize('completionsLabel', "Code completions"), false) : undefined; - const chatQuotaIndicator = chatQuota ? this.createQuotaIndicator(this.element, disposables, chatQuota, localize('chatsLabel', "Chat messages"), false) : undefined; - const premiumChatQuotaIndicator = premiumChatQuota ? this.createQuotaIndicator(this.element, disposables, premiumChatQuota, localize('premiumChatsLabel', "Premium requests"), true) : undefined; + const completionsQuotaIndicator = completionsQuota && (completionsQuota.total > 0 || completionsQuota.unlimited) ? this.createQuotaIndicator(this.element, disposables, completionsQuota, localize('completionsLabel', "Code completions"), false) : undefined; + const chatQuotaIndicator = chatQuota && (chatQuota.total > 0 || chatQuota.unlimited) ? this.createQuotaIndicator(this.element, disposables, chatQuota, localize('chatsLabel', "Chat messages"), false) : undefined; + const premiumChatQuotaIndicator = premiumChatQuota && (premiumChatQuota.total > 0 || premiumChatQuota.unlimited) ? this.createQuotaIndicator(this.element, disposables, premiumChatQuota, localize('premiumChatsLabel', "Premium requests"), true) : undefined; if (resetDate) { this.element.appendChild($('div.description', undefined, localize('limitQuota', "Allowance resets {0}.", this.dateFormatter.value.format(new Date(resetDate))))); @@ -386,7 +393,8 @@ class ChatStatusDashboard extends Disposable { // Settings { - addSeparator(localize('settingsTitle', "Settings"), this.chatEntitlementService.sentiment === ChatSentiment.Installed ? toAction({ + const chatSentiment = this.chatEntitlementService.sentiment; + addSeparator(localize('settingsTitle', "Settings"), chatSentiment.installed && !chatSentiment.disabled ? toAction({ id: 'workbench.action.openChatSettings', label: localize('settingsLabel', "Settings"), tooltip: localize('settingsTooltip', "Open Settings"), @@ -400,15 +408,34 @@ class ChatStatusDashboard extends Disposable { // New to Copilot / Signed out { const newUser = isNewUser(this.chatEntitlementService); + const disabled = this.chatEntitlementService.sentiment.disabled; const signedOut = this.chatEntitlementService.entitlement === ChatEntitlement.Unknown; - if (newUser || signedOut) { + if (newUser || signedOut || disabled) { addSeparator(); - this.element.appendChild($('div.description', undefined, newUser ? localize('activateDescription', "Set up Copilot to use AI features.") : localize('signInDescription', "Sign in to use Copilot AI features."))); + let descriptionText: string; + if (newUser) { + descriptionText = localize('activateDescription', "Set up Copilot to use AI features."); + } else if (disabled) { + descriptionText = localize('enableDescription', "Enable Copilot to use AI features."); + } else { + descriptionText = localize('signInDescription', "Sign in to use Copilot AI features."); + } + + let buttonLabel: string; + if (newUser) { + buttonLabel = localize('activateCopilotButton', "Set up Copilot"); + } else if (disabled) { + buttonLabel = localize('enableCopilotButton', "Enable Copilot"); + } else { + buttonLabel = localize('signInToUseCopilotButton', "Sign in to use Copilot"); + } + + this.element.appendChild($('div.description', undefined, descriptionText)); const button = disposables.add(new Button(this.element, { ...defaultButtonStyles })); - button.label = newUser ? localize('activateCopilotButton', "Set up Copilot") : localize('signInToUseCopilotButton', "Sign in to use Copilot"); - disposables.add(button.onDidClick(() => this.runCommandAndClose(newUser ? 'workbench.action.chat.triggerSetup' : () => this.chatEntitlementService.requests?.value.signIn()))); + button.label = buttonLabel; + disposables.add(button.onDidClick(() => this.runCommandAndClose('workbench.action.chat.triggerSetup'))); } } diff --git a/src/vs/workbench/contrib/chat/browser/chatViewPane.ts b/src/vs/workbench/contrib/chat/browser/chatViewPane.ts index cf16001e255..4c92a7e5aea 100644 --- a/src/vs/workbench/contrib/chat/browser/chatViewPane.ts +++ b/src/vs/workbench/contrib/chat/browser/chatViewPane.ts @@ -26,7 +26,7 @@ import { SIDE_BAR_FOREGROUND } from '../../../common/theme.js'; import { IViewDescriptorService } from '../../../common/views.js'; import { IChatViewTitleActionContext } from '../common/chatActions.js'; import { IChatAgentService } from '../common/chatAgents.js'; -import { ChatModelInitState, IChatModel } from '../common/chatModel.js'; +import { IChatModel } from '../common/chatModel.js'; import { CHAT_PROVIDER_ID } from '../common/chatParticipantContribTypes.js'; import { IChatService } from '../common/chatService.js'; import { ChatAgentLocation, ChatMode } from '../common/constants.js'; @@ -47,8 +47,6 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { private readonly modelDisposables = this._register(new DisposableStore()); private memento: Memento; private readonly viewState: IViewPaneState; - private defaultParticipantRegistrationFailed = false; - private didUnregisterProvider = false; private _restoringSession: Promise | undefined; @@ -112,18 +110,12 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { try { this._widget.setVisible(false); await this.updateModel(model, info.inputValue || info.mode ? { inputState: { chatMode: info.mode }, inputValue: info.inputValue } : undefined); - this.defaultParticipantRegistrationFailed = false; - this.didUnregisterProvider = false; - this._onDidChangeViewWelcomeState.fire(); } finally { this.widget.setVisible(wasVisible); } }); this._restoringSession.finally(() => this._restoringSession = undefined); } - } else if (this._widget?.viewModel?.initState === ChatModelInitState.Initialized) { - // Model is initialized, and the default agent disappeared, so show welcome view - this.didUnregisterProvider = true; } this._onDidChangeViewWelcomeState.fire(); @@ -161,8 +153,9 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { override shouldShowWelcome(): boolean { const noPersistedSessions = !this.chatService.hasSessions(); const hasCoreAgent = this.chatAgentService.getAgents().some(agent => agent.isCore && agent.locations.includes(this.chatOptions.location)); - const shouldShow = !hasCoreAgent && (this.didUnregisterProvider || !this._widget?.viewModel && noPersistedSessions || this.defaultParticipantRegistrationFailed); - this.logService.trace(`ChatViewPane#shouldShowWelcome(${this.chatOptions.location}) = ${shouldShow}: hasCoreAgent=${hasCoreAgent} didUnregister=${this.didUnregisterProvider} || noViewModel=${!this._widget?.viewModel} && noPersistedSessions=${noPersistedSessions} || defaultParticipantRegistrationFailed=${this.defaultParticipantRegistrationFailed}`); + const hasDefaultAgent = this.chatAgentService.getDefaultAgent(this.chatOptions.location) !== undefined; // only false when Hide Copilot has run and unregistered the setup agents + const shouldShow = !hasCoreAgent && (!hasDefaultAgent || !this._widget?.viewModel && noPersistedSessions); + this.logService.trace(`ChatViewPane#shouldShowWelcome(${this.chatOptions.location}) = ${shouldShow}: hasCoreAgent=${hasCoreAgent} hasDefaultAgent=${hasDefaultAgent} || noViewModel=${!this._widget?.viewModel} && noPersistedSessions=${noPersistedSessions}`); return !!shouldShow; } @@ -225,14 +218,6 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { this._widget.render(parent); const info = this.getTransferredOrPersistedSessionInfo(); - const disposeListener = this._register(this.chatService.onDidDisposeSession((e) => { - // Render the welcome view if provider registration fails, eg when signed out. This activates for any session, but the problem is the same regardless - if (e.reason === 'initializationFailed') { - this.defaultParticipantRegistrationFailed = true; - disposeListener?.dispose(); - this._onDidChangeViewWelcomeState.fire(); - } - })); const model = info.sessionId ? await this.chatService.getOrRestoreSession(info.sessionId) : undefined; await this.updateModel(model, info.inputValue || info.mode ? { inputState: { chatMode: info.mode }, inputValue: info.inputValue } : undefined); diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index 1d8bc8ce7c5..2bd3745c6fa 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -6,8 +6,6 @@ import * as dom from '../../../../base/browser/dom.js'; import { Button } from '../../../../base/browser/ui/button/button.js'; import { ITreeContextMenuEvent, ITreeElement } from '../../../../base/browser/ui/tree/tree.js'; -import { pick } from '../../../../base/common/arrays.js'; -import { assert } from '../../../../base/common/assert.js'; import { disposableTimeout, timeout } from '../../../../base/common/async.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { toErrorMessage } from '../../../../base/common/errorMessage.js'; @@ -18,16 +16,15 @@ import { Iterable } from '../../../../base/common/iterator.js'; import { combinedDisposable, Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { ResourceSet } from '../../../../base/common/map.js'; import { Schemas } from '../../../../base/common/network.js'; -import { autorun, autorunWithStore, observableFromEvent, observableValue } from '../../../../base/common/observable.js'; +import { autorun, observableFromEvent, observableValue } from '../../../../base/common/observable.js'; import { basename, extUri, isEqual } from '../../../../base/common/resources.js'; import { isDefined } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; -import { isLocation, Location } from '../../../../editor/common/languages.js'; +import { isLocation } from '../../../../editor/common/languages.js'; import { localize } from '../../../../nls.js'; import { MenuId } from '../../../../platform/actions/common/actions.js'; -import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; @@ -48,7 +45,7 @@ import { applyingChatEditsFailedContextKey, decidedChatEditingResourceContextKey import { ChatPauseState, IChatModel, IChatRequestVariableEntry, IChatResponseModel } from '../common/chatModel.js'; import { chatAgentLeader, ChatRequestAgentPart, ChatRequestDynamicVariablePart, ChatRequestSlashPromptPart, ChatRequestToolPart, chatSubcommandLeader, formatChatQuestion, IParsedChatRequest } from '../common/chatParserTypes.js'; import { ChatRequestParser } from '../common/chatRequestParser.js'; -import { IChatFollowup, IChatLocationData, IChatSendRequestOptions, IChatService } from '../common/chatService.js'; +import { IChatLocationData, IChatSendRequestOptions, IChatService } from '../common/chatService.js'; import { IChatSlashCommandService } from '../common/chatSlashCommands.js'; import { ChatViewModel, IChatResponseViewModel, isRequestVM, isResponseVM } from '../common/chatViewModel.js'; import { IChatInputState } from '../common/chatWidgetHistoryService.js'; @@ -56,7 +53,7 @@ import { CodeBlockModelCollection } from '../common/codeBlockModelCollection.js' import { ChatAgentLocation, ChatMode } from '../common/constants.js'; import { ILanguageModelToolsService } from '../common/languageModelToolsService.js'; import { IPromptsService } from '../common/promptSyntax/service/types.js'; -import { IToggleChatModeArgs, ToggleAgentModeActionId } from './actions/chatExecuteActions.js'; +import { handleModeSwitch } from './actions/chatActions.js'; import { ChatTreeItem, IChatAcceptInputOptions, IChatAccessibilityService, IChatCodeBlockInfo, IChatFileTreeInfo, IChatListItemRendererOptions, IChatWidget, IChatWidgetService, IChatWidgetViewContext, IChatWidgetViewOptions } from './chat.js'; import { ChatAccessibilityProvider } from './chatAccessibilityProvider.js'; import { ChatAttachmentModel } from './chatAttachmentModel.js'; @@ -272,7 +269,6 @@ export class ChatWidget extends Disposable implements IChatWidget { @IChatEditingService chatEditingService: IChatEditingService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IPromptsService private readonly promptsService: IPromptsService, - @ICommandService private readonly commandService: ICommandService, @ILanguageModelToolsService private readonly toolsService: ILanguageModelToolsService, ) { super(); @@ -345,7 +341,7 @@ export class ChatWidget extends Disposable implements IChatWidget { } })); - this._register(autorunWithStore((r, store) => { + this._register(autorun(r => { const viewModel = viewModelObs.read(r); const sessions = chatEditingService.editingSessionsObs.read(r); @@ -366,14 +362,14 @@ export class ChatWidget extends Disposable implements IChatWidget { this._editingSession.set(session, undefined); - store.add(session.onDidDispose(() => { + r.store.add(session.onDidDispose(() => { this._editingSession.set(undefined, undefined); this.renderChatEditingSessionState(); })); - store.add(this.onDidChangeParsedInput(() => { + r.store.add(this.onDidChangeParsedInput(() => { this.renderChatEditingSessionState(); })); - store.add(this.inputEditor.onDidChangeModelContent(() => { + r.store.add(this.inputEditor.onDidChangeModelContent(() => { if (this.getInput() === '') { this.refreshParsedInput(); this.renderChatEditingSessionState(); @@ -501,6 +497,7 @@ export class ChatWidget extends Disposable implements IChatWidget { this.container = dom.append(parent, $('.interactive-session')); this.welcomeMessageContainer = dom.append(this.container, $('.chat-welcome-view-container', { style: 'display: none' })); + this._register(dom.addStandardDisposableListener(this.welcomeMessageContainer, dom.EventType.CLICK, () => this.focusInput())); if (renderInputOnTop) { this.createInput(this.container, { renderFollowups, renderStyle }); this.listContainer = dom.append(this.container, $(`.interactive-list`)); @@ -676,13 +673,7 @@ export class ChatWidget extends Disposable implements IChatWidget { this.layoutDynamicChatTreeItemMode(); } - if (this.lastItem && isResponseVM(this.lastItem) && this.lastItem.isComplete) { - this.renderFollowups(this.lastItem.replyFollowups, this.lastItem); - } else if (!treeItems.length && this.viewModel) { - this.renderSampleQuestions(); - } else { - this.renderFollowups(undefined); - } + this.renderFollowups(); } } @@ -751,15 +742,12 @@ export class ChatWidget extends Disposable implements IChatWidget { } } - private renderSampleQuestions() { - if (this.viewModel?.getItems().length === 0) { - // TODO@roblourens hack- only Chat mode supports sample questions - this.renderFollowups(this.input.currentMode === ChatMode.Ask ? this.viewModel.model.sampleQuestions : undefined); + private async renderFollowups(): Promise { + if (this.lastItem && isResponseVM(this.lastItem) && this.lastItem.isComplete && this.inputPart.currentMode === ChatMode.Ask) { + this.inputPart.renderFollowups(this.lastItem.replyFollowups, this.lastItem); + } else { + this.inputPart.renderFollowups(undefined, undefined); } - } - - private async renderFollowups(items: IChatFollowup[] | undefined, response?: IChatResponseViewModel): Promise { - this.inputPart.renderFollowups(items, response); if (this.bodyDimension) { this.layout(this.bodyDimension.height, this.bodyDimension.width); @@ -1031,9 +1019,9 @@ export class ChatWidget extends Disposable implements IChatWidget { this.renderWelcomeViewContentIfNeeded(); })); this._register(this.input.onDidChangeCurrentChatMode(() => { - this.renderSampleQuestions(); this.renderWelcomeViewContentIfNeeded(); this.refreshParsedInput(); + this.renderFollowups(); })); this._register(autorun(r => { const enabledTools = new Set(this.input.selectedToolsModel.tools.read(r).map(t => t.id)); @@ -1208,7 +1196,7 @@ export class ChatWidget extends Disposable implements IChatWidget { return input; } - if (!attachedContext.some(variable => isPromptFileChatVariable(variable) && isEqual(toUri(variable), promptPath.uri))) { + if (!attachedContext.some(variable => isPromptFileChatVariable(variable) && isEqual(URI.isUri(variable.value) ? variable.value : variable.value.uri, promptPath.uri))) { // not yet attached, so attach it const variable = toChatVariable({ uri: promptPath.uri, isPromptFile: true }, true); attachedContext.push(variable); @@ -1238,7 +1226,11 @@ export class ChatWidget extends Disposable implements IChatWidget { if (instructionsEnabled) { input = await this._handlePromptSlashCommand(input, attachedContext); await this.autoAttachInstructions(attachedContext); - input = await this.setupChatModeAndTools(input, attachedContext); + const newInput = await this.setupChatModeAndTools(input, attachedContext); + if (newInput === undefined) { + return; + } + input = newInput; } if (this.viewOptions.enableWorkingSet !== undefined && this.input.currentMode === ChatMode.Edit && !this.chatService.edits2Enabled) { @@ -1511,35 +1503,31 @@ export class ChatWidget extends Disposable implements IChatWidget { this.agentInInput.set(!!currentAgent); } - /** - * Set's up the `chat mode` and selects required `tools` based on - * the metadata defined in headers of attached prompt files. - */ - private async setupChatModeAndTools( - input: string, - attachedContext: readonly IChatRequestVariableEntry[], - ): Promise { + private async setupChatModeAndTools(input: string, attachedContext: readonly IChatRequestVariableEntry[],): Promise { // process prompt files starting from the 'root' ones - const promptFileVariables = attachedContext - .filter(isPromptFileChatVariable) - .filter(pick('isRoot')); - const promptUris = promptFileVariables.map(toUri); + const promptUris: URI[] = []; + for (const item of attachedContext) { + if (isPromptFileChatVariable(item) && item.isRoot) { + if (URI.isUri(item.value)) { + promptUris.push(item.value); + } else { + promptUris.push(item.value.uri); + } + } + } - if (promptFileVariables.length === 0) { + if (promptUris.length === 0) { return input; } if (!input.trim()) { - const promptNames = (promptUris.length === 1) - ? `'${basename(promptUris[0])}'` - : `the prompt files`; - - input = `Follow instructions from ${promptNames}.`; + input = promptUris.length === 1 + ? localize('input.1', "Follow instructions from ${0}.", basename(promptUris[0])) + : localize('input.N', "Follow instructions from the prompt files."); } - const metadata = await this.promptsService - .getCombinedToolsMetadata(promptUris); + const metadata = await this.promptsService.getCombinedToolsMetadata(promptUris); if (metadata === null) { return input; @@ -1549,10 +1537,14 @@ export class ChatWidget extends Disposable implements IChatWidget { // switch to appropriate chat mode if needed if (mode && mode !== this.inputPart.currentMode) { - await this.commandService.executeCommand( - ToggleAgentModeActionId, - { mode } satisfies IToggleChatModeArgs, - ); + const chatModeCheck = await this.instantiationService.invokeFunction(handleModeSwitch, this.inputPart.currentMode, mode, this.viewModel?.model.getRequests().length ?? 0, this.viewModel?.model.editingSession); + if (!chatModeCheck) { + return undefined; + } else if (chatModeCheck.needToClearSession) { + this.clear(); + await this.waitForReady(); + } + this.inputPart.setChatMode(mode); } // if not tools to enable are present, we are done @@ -1560,34 +1552,17 @@ export class ChatWidget extends Disposable implements IChatWidget { return input; } - // sanity check on the logic of the `getPromptFilesMetadata` method - // and the code above in case this block is moved around somewhere else: - // if we have some tools present, the mode must have been equal to `agent` - assert( - this.inputPart.currentMode === ChatMode.Agent, - `Chat mode must be 'agent' when there are 'tools' defined, got ${this.inputPart.currentMode}.`, - ); - // convert tools names to tool IDs - const toolIds = tools - .map((toolName) => { - const tool = this.toolsService.getToolByName(toolName); - - if (tool === undefined) { - this.logService.warn( - `[setup tools]: cannot to find tool '${toolName}'`, - ); - } - - return tool; - }) - .filter(isDefined) - .map(pick('id')); + const toolIds: string[] = []; + for (const toolName of tools) { + const tool = this.toolsService.getToolByName(toolName); + if (tool) { + toolIds.push(tool.id); + } + } // if there are some tools defined in the prompt files, select only the specified tools - this.inputPart - .selectedToolsModel - .selectOnly(toolIds); + this.inputPart.selectedToolsModel.selectOnly(toolIds); return input; } @@ -1597,12 +1572,16 @@ export class ChatWidget extends Disposable implements IChatWidget { * match file references in the attached context and then attaches * such instructions to the context. */ - private async autoAttachInstructions( - attachedContext: IChatRequestVariableEntry[], - ): Promise { - const variableUris = attachedContext - .filter(hasAddressableValue) - .map(toUri); + private async autoAttachInstructions(attachedContext: IChatRequestVariableEntry[]): Promise { + + const variableUris: URI[] = []; + for (const item of attachedContext) { + if (URI.isUri(item.value)) { + variableUris.push(item.value); + } else if (isLocation(item.value)) { + variableUris.push(item.value.uri); + } + } const automaticInstructions = await this.promptsService .findInstructionFilesFor(variableUris); @@ -1621,42 +1600,7 @@ export class ChatWidget extends Disposable implements IChatWidget { } } -/** - * Type for any "addressable" object - i.e., an object that has - * the `value` property that is either a {@link URI} or a {@link Location}. - */ -export type TAddressable = T & { value: URI | Location }; -/** - * Check if provided object is "addressable" - i.e., has the `value` - * property that is either a {@link URI} or a {@link Location}. - */ -const hasAddressableValue = ( - thing: T, -): thing is TAddressable => { - if ((!thing) || (('value' in thing) === false)) { - return false; - } - - if (URI.isUri(thing.value) || isLocation(thing.value)) { - return true; - } - - return false; -}; - -/** - * Returns URI of a provided "addressable" object. - */ -const toUri = ( - thing: TAddressable, -): URI => { - const { value } = thing; - - return URI.isUri(value) - ? value - : value.uri; -}; export class ChatWidgetService extends Disposable implements IChatWidgetService { diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts index 9423d3b995e..8c3f93517f1 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts @@ -4,33 +4,18 @@ *--------------------------------------------------------------------------------------------*/ import { coalesce } from '../../../../../base/common/arrays.js'; -import { CancellationToken } from '../../../../../base/common/cancellation.js'; -import { Codicon } from '../../../../../base/common/codicons.js'; -import { isCancellationError } from '../../../../../base/common/errors.js'; -import * as glob from '../../../../../base/common/glob.js'; import { IMarkdownString, MarkdownString } from '../../../../../base/common/htmlContent.js'; -import { Disposable, DisposableStore, dispose, isDisposable } from '../../../../../base/common/lifecycle.js'; -import { ResourceSet } from '../../../../../base/common/map.js'; -import { basename, dirname, extUri, joinPath, relativePath } from '../../../../../base/common/resources.js'; -import { ThemeIcon } from '../../../../../base/common/themables.js'; +import { Disposable, dispose, isDisposable } from '../../../../../base/common/lifecycle.js'; import { URI } from '../../../../../base/common/uri.js'; import { IRange, Range } from '../../../../../editor/common/core/range.js'; import { IDecorationOptions } from '../../../../../editor/common/editorCommon.js'; import { Command, isLocation } from '../../../../../editor/common/languages.js'; -import { ILanguageService } from '../../../../../editor/common/languages/language.js'; -import { getIconClasses } from '../../../../../editor/common/services/getIconClasses.js'; -import { IModelService } from '../../../../../editor/common/services/model.js'; import { Action2, registerAction2 } from '../../../../../platform/actions/common/actions.js'; import { ICommandService } from '../../../../../platform/commands/common/commands.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; -import { FileKind, FileType, IFileService } from '../../../../../platform/files/common/files.js'; import { IInstantiationService, ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; import { ILabelService } from '../../../../../platform/label/common/label.js'; import { PromptsConfig } from '../../../../../platform/prompts/common/config.js'; -import { IQuickInputService, IQuickPickItem } from '../../../../../platform/quickinput/common/quickInput.js'; -import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; -import { IHistoryService } from '../../../../services/history/common/history.js'; -import { getExcludes, IFileQuery, ISearchComplete, ISearchConfiguration, ISearchService, QueryType } from '../../../../services/search/common/search.js'; import { IChatRequestVariableValue, IDynamicVariable } from '../../common/chatVariables.js'; import { IChatWidget } from '../chat.js'; import { ChatWidget, IChatWidgetContrib } from '../chatWidget.js'; @@ -239,216 +224,6 @@ function isDynamicVariable(obj: any): obj is IDynamicVariable { ChatWidget.CONTRIBS.push(ChatDynamicVariableModel); -export async function createFilesAndFolderQuickPick(accessor: ServicesAccessor): Promise { - const quickInputService = accessor.get(IQuickInputService); - const searchService = accessor.get(ISearchService); - const configurationService = accessor.get(IConfigurationService); - const workspaceService = accessor.get(IWorkspaceContextService); - const fileService = accessor.get(IFileService); - const labelService = accessor.get(ILabelService); - const modelService = accessor.get(IModelService); - const languageService = accessor.get(ILanguageService); - const historyService = accessor.get(IHistoryService); - - type ResourcePick = IQuickPickItem & { resource: URI; kind: FileKind }; - - const workspaces = workspaceService.getWorkspace().folders.map(folder => folder.uri); - - const defaultItems: ResourcePick[] = []; - (await getTopLevelFolders(workspaces, fileService)).forEach(uri => defaultItems.push(createQuickPickItem(uri, FileKind.FOLDER))); - historyService.getHistory().filter(a => a.resource).slice(0, 30).forEach(uri => defaultItems.push(createQuickPickItem(uri.resource!, FileKind.FILE))); - defaultItems.sort((a, b) => extUri.compare(a.resource, b.resource)); - - const quickPick = quickInputService.createQuickPick(); - quickPick.placeholder = 'Search file or folder by name'; - quickPick.items = defaultItems; - - return await new Promise(_resolve => { - - const disposables = new DisposableStore(); - const resolve = (res: URI | undefined) => { - _resolve(res); - disposables.dispose(); - quickPick.dispose(); - }; - - disposables.add(quickPick.onDidChangeValue(async value => { - if (value === '') { - quickPick.items = defaultItems; - return; - } - - const picks: ResourcePick[] = []; - - await Promise.all(workspaces.map(async workspace => { - const result = await searchFilesAndFolders( - workspace, - value, - true, - undefined, - undefined, - configurationService, - searchService - ); - - for (const folder of result.folders) { - picks.push(createQuickPickItem(folder, FileKind.FOLDER)); - } - for (const file of result.files) { - picks.push(createQuickPickItem(file, FileKind.FILE)); - } - })); - - quickPick.items = picks.sort((a, b) => extUri.compare(a.resource, b.resource)); - })); - - disposables.add(quickPick.onDidAccept((e) => { - const value = (quickPick.selectedItems[0] as any)?.resource; - resolve(value); - })); - - disposables.add(quickPick.onDidHide(() => { - resolve(undefined); - })); - - quickPick.show(); - }); - - function createQuickPickItem(resource: URI, kind: FileKind): ResourcePick { - return { - resource, - kind, - id: resource.toString(), - alwaysShow: true, - label: basename(resource), - description: labelService.getUriLabel(dirname(resource), { relative: true }), - iconClasses: kind === FileKind.FILE ? getIconClasses(modelService, languageService, resource, FileKind.FILE) : undefined, - iconClass: kind === FileKind.FOLDER ? ThemeIcon.asClassName(Codicon.folder) : undefined - }; - } -} - -export async function getTopLevelFolders(workspaces: URI[], fileService: IFileService): Promise { - const folders: URI[] = []; - for (const workspace of workspaces) { - const fileSystemProvider = fileService.getProvider(workspace.scheme); - if (!fileSystemProvider) { - continue; - } - - const entries = await fileSystemProvider.readdir(workspace); - for (const [name, type] of entries) { - const entryResource = joinPath(workspace, name); - if (type === FileType.Directory) { - folders.push(entryResource); - } - } - } - - return folders; -} - -export async function searchFilesAndFolders( - workspace: URI, - pattern: string, - fuzzyMatch: boolean, - token: CancellationToken | undefined, - cacheKey: string | undefined, - configurationService: IConfigurationService, - searchService: ISearchService -): Promise<{ folders: URI[]; files: URI[] }> { - const segmentMatchPattern = caseInsensitiveGlobPattern(fuzzyMatch ? fuzzyMatchingGlobPattern(pattern) : continousMatchingGlobPattern(pattern)); - - const searchExcludePattern = getExcludes(configurationService.getValue({ resource: workspace })) || {}; - const searchOptions: IFileQuery = { - folderQueries: [{ - folder: workspace, - disregardIgnoreFiles: configurationService.getValue('explorer.excludeGitIgnore'), - }], - type: QueryType.File, - shouldGlobMatchFilePattern: true, - cacheKey, - excludePattern: searchExcludePattern, - sortByScore: true, - }; - - let searchResult: ISearchComplete | undefined; - try { - searchResult = await searchService.fileSearch({ ...searchOptions, filePattern: `{**/${segmentMatchPattern}/**,${pattern}}` }, token); - } catch (e) { - if (!isCancellationError(e)) { - throw e; - } - } - - if (!searchResult || token?.isCancellationRequested) { - return { files: [], folders: [] }; - } - - const fileResources = searchResult.results.map(result => result.resource); - const folderResources = getMatchingFoldersFromFiles(fileResources, workspace, segmentMatchPattern); - - return { folders: folderResources, files: fileResources }; -} - -function fuzzyMatchingGlobPattern(pattern: string): string { - if (!pattern) { - return '*'; - } - return '*' + pattern.split('').join('*') + '*'; -} - -function continousMatchingGlobPattern(pattern: string): string { - if (!pattern) { - return '*'; - } - return '*' + pattern + '*'; -} - -function caseInsensitiveGlobPattern(pattern: string): string { - let caseInsensitiveFilePattern = ''; - for (let i = 0; i < pattern.length; i++) { - const char = pattern[i]; - if (/[a-zA-Z]/.test(char)) { - caseInsensitiveFilePattern += `[${char.toLowerCase()}${char.toUpperCase()}]`; - } else { - caseInsensitiveFilePattern += char; - } - } - return caseInsensitiveFilePattern; -} - - -// TODO: remove this and have support from the search service -function getMatchingFoldersFromFiles(resources: URI[], workspace: URI, segmentMatchPattern: string): URI[] { - const uniqueFolders = new ResourceSet(); - for (const resource of resources) { - const relativePathToRoot = relativePath(workspace, resource); - if (!relativePathToRoot) { - throw new Error('Resource is not a child of the workspace'); - } - - let dirResource = workspace; - const stats = relativePathToRoot.split('/').slice(0, -1); - for (const stat of stats) { - dirResource = dirResource.with({ path: `${dirResource.path}/${stat}` }); - uniqueFolders.add(dirResource); - } - } - - const matchingFolders: URI[] = []; - for (const folderResource of uniqueFolders) { - const stats = folderResource.path.split('/'); - const dirStat = stats[stats.length - 1]; - if (!dirStat || !glob.match(segmentMatchPattern, dirStat)) { - continue; - } - - matchingFolders.push(folderResource); - } - - return matchingFolders; -} export interface IAddDynamicVariableContext { diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables/chatFileReference.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables/chatFileReference.ts index 7fe2a6d72dd..a7e4d40fa61 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables/chatFileReference.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables/chatFileReference.ts @@ -23,7 +23,7 @@ export class ChatFileReference extends FilePromptParser implements IDynamicVaria */ constructor( public readonly reference: IDynamicVariable, - @IInstantiationService initService: IInstantiationService, + @IInstantiationService instantiationService: IInstantiationService, @IWorkspaceContextService workspaceService: IWorkspaceContextService, @ILogService logService: ILogService, ) { @@ -34,7 +34,7 @@ export class ChatFileReference extends FilePromptParser implements IDynamicVaria `Variable data must be an URI, got '${data}'.`, ); - super(data, {}, initService, workspaceService, logService); + super(data, {}, instantiationService, workspaceService, logService); } /** diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts index 9b532a8c7b4..466f2faff8f 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts @@ -10,7 +10,7 @@ import { Schemas } from '../../../../../base/common/network.js'; import { autorun } from '../../../../../base/common/observable.js'; import { basename, isEqual } from '../../../../../base/common/resources.js'; import { URI } from '../../../../../base/common/uri.js'; -import { ICodeEditor, isCodeEditor, isDiffEditor } from '../../../../../editor/browser/editorBrowser.js'; +import { getCodeEditor, ICodeEditor } from '../../../../../editor/browser/editorBrowser.js'; import { ICodeEditorService } from '../../../../../editor/browser/services/codeEditorService.js'; import { Location } from '../../../../../editor/common/languages.js'; import { IModelService } from '../../../../../editor/common/services/model.js'; @@ -33,9 +33,9 @@ import { toChatVariable } from '../chatAttachmentModel/chatPromptAttachmentsColl export class ChatImplicitContextContribution extends Disposable implements IWorkbenchContribution { static readonly ID = 'chat.implicitContext'; - private readonly _currentCancelTokenSource = this._register(new MutableDisposable()); + private readonly _currentCancelTokenSource: MutableDisposable; - private _implicitContextEnablement = this.configurationService.getValue<{ [mode: string]: string }>('chat.implicitContext.enabled'); + private _implicitContextEnablement: { [mode: string]: string }; constructor( @ICodeEditorService private readonly codeEditorService: ICodeEditorService, @@ -47,6 +47,8 @@ export class ChatImplicitContextContribution extends Disposable implements IWork @ILanguageModelIgnoredFilesService private readonly ignoredFilesService: ILanguageModelIgnoredFilesService, ) { super(); + this._currentCancelTokenSource = this._register(new MutableDisposable()); + this._implicitContextEnablement = this.configurationService.getValue<{ [mode: string]: string }>('chat.implicitContext.enabled'); const activeEditorDisposables = this._register(new DisposableStore()); @@ -131,12 +133,8 @@ export class ChatImplicitContextContribution extends Disposable implements IWork } } for (const codeOrDiffEditor of this.editorService.getVisibleTextEditorControls(EditorsOrder.MOST_RECENTLY_ACTIVE)) { - let codeEditor: ICodeEditor; - if (isDiffEditor(codeOrDiffEditor)) { - codeEditor = codeOrDiffEditor.getModifiedEditor(); - } else if (isCodeEditor(codeOrDiffEditor)) { - codeEditor = codeOrDiffEditor; - } else { + const codeEditor = getCodeEditor(codeOrDiffEditor); + if (!codeEditor) { continue; } diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts index 791c3d5b656..c89332e219c 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts @@ -9,9 +9,11 @@ import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { isPatternInWord } from '../../../../../base/common/filters.js'; import { Disposable } from '../../../../../base/common/lifecycle.js'; import { ResourceSet } from '../../../../../base/common/map.js'; +import { Schemas } from '../../../../../base/common/network.js'; import { URI } from '../../../../../base/common/uri.js'; import { generateUuid } from '../../../../../base/common/uuid.js'; -import { isCodeEditor } from '../../../../../editor/browser/editorBrowser.js'; +import { ICodeEditor, getCodeEditor, isCodeEditor } from '../../../../../editor/browser/editorBrowser.js'; +import { ICodeEditorService } from '../../../../../editor/browser/services/codeEditorService.js'; import { Position } from '../../../../../editor/common/core/position.js'; import { Range } from '../../../../../editor/common/core/range.js'; import { IWordAtPosition, getWordAtText } from '../../../../../editor/common/core/wordHelper.js'; @@ -29,10 +31,12 @@ import { ILabelService } from '../../../../../platform/label/common/label.js'; import { Registry } from '../../../../../platform/registry/common/platform.js'; import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from '../../../../common/contributions.js'; +import { EditorsOrder } from '../../../../common/editor.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { IHistoryService } from '../../../../services/history/common/history.js'; import { LifecyclePhase } from '../../../../services/lifecycle/common/lifecycle.js'; import { ISearchService } from '../../../../services/search/common/search.js'; +import { searchFilesAndFolders } from '../../../search/browser/chatContributions.js'; import { IChatAgentData, IChatAgentNameService, IChatAgentService, getFullyQualifiedId } from '../../common/chatAgents.js'; import { IChatEditingService } from '../../common/chatEditingService.js'; import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashPromptPart, ChatRequestTextPart, ChatRequestToolPart, chatAgentLeader, chatSubcommandLeader, chatVariableLeader } from '../../common/chatParserTypes.js'; @@ -43,7 +47,7 @@ import { IPromptsService } from '../../common/promptSyntax/service/types.js'; import { ChatSubmitAction } from '../actions/chatExecuteActions.js'; import { IChatWidget, IChatWidgetService } from '../chat.js'; import { ChatInputPart } from '../chatInputPart.js'; -import { ChatDynamicVariableModel, searchFilesAndFolders } from './chatDynamicVariables.js'; +import { ChatDynamicVariableModel } from './chatDynamicVariables.js'; class SlashCommandCompletions extends Disposable { constructor( @@ -511,6 +515,7 @@ class BuiltinDynamicCompletions extends Disposable { @IOutlineModelService private readonly outlineService: IOutlineModelService, @IEditorService private readonly editorService: IEditorService, @IConfigurationService private readonly configurationService: IConfigurationService, + @ICodeEditorService private readonly codeEditorService: ICodeEditorService, ) { super(); @@ -536,7 +541,7 @@ class BuiltinDynamicCompletions extends Disposable { return; } - const active = this.editorService.activeTextEditorControl; + const active = this.findActiveCodeEditor(); if (!isCodeEditor(active)) { return; } @@ -590,6 +595,32 @@ class BuiltinDynamicCompletions extends Disposable { this._register(CommandsRegistry.registerCommand(BuiltinDynamicCompletions.addReferenceCommand, (_services, arg) => this.cmdAddReference(arg))); } + private findActiveCodeEditor(): ICodeEditor | undefined { + const codeEditor = this.codeEditorService.getActiveCodeEditor(); + if (codeEditor) { + const model = codeEditor.getModel(); + if (model?.uri.scheme === Schemas.vscodeNotebookCell) { + return undefined; + } + + if (model) { + return codeEditor; + } + } + for (const codeOrDiffEditor of this.editorService.getVisibleTextEditorControls(EditorsOrder.MOST_RECENTLY_ACTIVE)) { + const codeEditor = getCodeEditor(codeOrDiffEditor); + if (!codeEditor) { + continue; + } + + const model = codeEditor.getModel(); + if (model) { + return codeEditor; + } + } + return undefined; + } + private registerVariableCompletions(debugName: string, provider: (details: IVariableCompletionsDetails, token: CancellationToken) => ProviderResult, wordPattern: RegExp = BuiltinDynamicCompletions.VariableNameDef) { this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { _debugDisplayName: `chatVarCompletions-${debugName}`, diff --git a/src/vs/workbench/contrib/chat/browser/imageUtils.ts b/src/vs/workbench/contrib/chat/browser/imageUtils.ts index caa02352006..2dbfd747907 100644 --- a/src/vs/workbench/contrib/chat/browser/imageUtils.ts +++ b/src/vs/workbench/contrib/chat/browser/imageUtils.ts @@ -24,7 +24,7 @@ export async function resizeImage(data: Uint8Array | string, mimeType?: string): } return new Promise((resolve, reject) => { - const blob = new Blob([data], { type: mimeType }); + const blob = new Blob([data as Uint8Array], { type: mimeType }); const img = new Image(); const url = URL.createObjectURL(blob); img.src = url; diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 44e238bd447..5c8b384d881 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -188,9 +188,19 @@ .interactive-item-container.interactive-response:not(.chat-response-loading) .chat-footer-toolbar { /* Complete response only */ - display: initial; - padding-top: 6px; - height: 22px; + display: block; + opacity: 0; + visibility: hidden; + transition: opacity 0.2s ease, visibility 0.2s ease; + padding-top: 6px; + height: 22px; +} + +/* Show toolbar on hover and last response */ +.interactive-item-container.interactive-response:not(.chat-response-loading):hover .chat-footer-toolbar, +.interactive-item-container.interactive-response.chat-most-recent-response:not(.chat-response-loading) .chat-footer-toolbar { + opacity: 1; + visibility: visible; } .interactive-item-container .value { @@ -906,7 +916,7 @@ have to be updated for changes to the rules above, or to support more deeply nes margin-right: auto; } -.interactive-session .chat-input-toolbars > .chat-execute-toolbar { +.interactive-session .chat-input-toolbars > .chat-input-toolbar { min-width: 0px; .chat-modelPicker-item { diff --git a/src/vs/workbench/contrib/chat/browser/media/chatSetup.css b/src/vs/workbench/contrib/chat/browser/media/chatSetup.css index 57238c0c4a9..4a06d47df07 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatSetup.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatSetup.css @@ -3,47 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.chat-setup-dialog { - - p { - margin-top: 0; - margin-bottom: 0; - width: 100%; - } +.chat-setup-dialog-footer { p.setup-settings { - font-size: 12px; + font-size: 11px; color: var(--vscode-descriptionForeground); - margin-top: 1em; - } - - .chat-feature-container { - display: flex; - align-items: center; - gap: 6px; - padding: 5px 10px 5px 10px; - } - - .chat-feature-container .codicon[class*='codicon-'] { - font-size: 16px; - } - - .codicon[class*='codicon-'] { - font-size: 13px; - line-height: 1.4em; - vertical-align: bottom; - } - - .button-container { - padding-top: 20px; - } - - /** Dropdown Button */ - .monaco-button-dropdown { - width: 100%; - } - - .monaco-button-dropdown .monaco-text-button { - width: 100%; } } diff --git a/src/vs/workbench/contrib/chat/browser/media/simpleBrowserOverlay.css b/src/vs/workbench/contrib/chat/browser/media/simpleBrowserOverlay.css index ad5d4e5f4d2..9858b1d3b26 100644 --- a/src/vs/workbench/contrib/chat/browser/media/simpleBrowserOverlay.css +++ b/src/vs/workbench/contrib/chat/browser/media/simpleBrowserOverlay.css @@ -32,12 +32,17 @@ right: 15px; } -.element-selection-cancel, -.element-selection-start { +.element-selection-cancel { padding: 2px 8px; width: fit-content; } +.element-selection-message .monaco-button-dropdown > .monaco-button.monaco-text-button { + height: 24px; + align-content: center; + padding: 0px 5px; +} + .element-selection-message .monaco-button.codicon.codicon-close, .element-expand-container .monaco-button.codicon.codicon-layout, .element-selection-message .monaco-button.codicon.codicon-chevron-right, diff --git a/src/vs/workbench/contrib/chat/browser/modelPicker/modePickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/modelPicker/modePickerActionItem.ts index 4b4d7d9f494..a3888321f72 100644 --- a/src/vs/workbench/contrib/chat/browser/modelPicker/modePickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/modelPicker/modePickerActionItem.ts @@ -64,7 +64,7 @@ export class ModePickerActionItem extends ActionWidgetDropdownActionViewItem { const modelPickerActionWidgetOptions: Omit = { actionProvider, - showItemKeybindings: false + showItemKeybindings: true }; super(action, modelPickerActionWidgetOptions, actionWidgetService, keybindingService, contextKeyService); diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/attachInstructionsCommand.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/attachInstructionsCommand.ts deleted file mode 100644 index d6a8b99f892..00000000000 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/attachInstructionsCommand.ts +++ /dev/null @@ -1,117 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { localize } from '../../../../../../nls.js'; -import { URI } from '../../../../../../base/common/uri.js'; -import { CHAT_CATEGORY } from '../../actions/chatActions.js'; -import { IChatWidget, IChatWidgetService } from '../../chat.js'; -import { ChatContextKeys } from '../../../common/chatContextKeys.js'; -import { KeyMod, KeyCode } from '../../../../../../base/common/keyCodes.js'; -import { runAttachInstructionsAction } from '../../actions/promptActions/index.js'; -import { PromptsConfig } from '../../../../../../platform/prompts/common/config.js'; -import { INSTRUCTIONS_LANGUAGE_ID } from '../../../common/promptSyntax/constants.js'; -import { ICommandService } from '../../../../../../platform/commands/common/commands.js'; -import { ContextKeyExpr } from '../../../../../../platform/contextkey/common/contextkey.js'; -import { MenuId, MenuRegistry } from '../../../../../../platform/actions/common/actions.js'; -import { ServicesAccessor } from '../../../../../../platform/instantiation/common/instantiation.js'; -import { ICodeEditorService } from '../../../../../../editor/browser/services/codeEditorService.js'; -import { KeybindingsRegistry, KeybindingWeight } from '../../../../../../platform/keybinding/common/keybindingsRegistry.js'; - -/** - * Command ID of the "Attach Instructions" command. - */ -export const INSTRUCTIONS_COMMAND_ID = 'workbench.command.instructions.attach'; - -/** - * Keybinding of the "Use Instructions" command. - * The `cmd + /` is the current keybinding for 'attachment', so we use - * the `alt` key modifier to convey the "instructions attachment" action. - */ -const INSTRUCTIONS_COMMAND_KEY_BINDING = KeyMod.CtrlCmd | KeyCode.Slash | KeyMod.Alt; - -/** - * Implementation of the "Use Instructions" command. The command works in the following way. - * - * When executed, it tries to see if a `prompt file` was open in the active code editor - * (see {@link IChatAttachPromptActionOptions.resource resource}), and if a chat input - * is focused (see {@link IChatAttachPromptActionOptions.widget widget}). - * - * Then the command shows prompt selection dialog to the user. If an active prompt file - * was detected, it is pre-selected in the dialog. User can confirm (`enter`) or select - * a different prompt file in the dialog. - * - * When a prompt file is selected by the user (or confirmed), the command attaches - * the selected prompt to the focused chat input, if present. If no focused chat input - * is present, the command would attach the prompt to a `chat panel` input by default - * (either the last focused instance, or a new one). If the `alt` (`option` on mac) key - * was pressed when the prompt was selected, a `chat edits` panel is used instead - * (likewise either the last focused or a new one). - */ -const command = async ( - accessor: ServicesAccessor, -): Promise => { - const commandService = accessor.get(ICommandService); - - await runAttachInstructionsAction(commandService, { - resource: getActiveInstructionsFileUri(accessor), - widget: getFocusedChatWidget(accessor), - }); -}; - -/** - * Get chat widget reference to attach instructions to. - */ -export function getFocusedChatWidget(accessor: ServicesAccessor): IChatWidget | undefined { - const chatWidgetService = accessor.get(IChatWidgetService); - - const { lastFocusedWidget } = chatWidgetService; - if (!lastFocusedWidget) { - return undefined; - } - - // the widget input `must` be focused at the time when command run - if (!lastFocusedWidget.hasInputFocus()) { - return undefined; - } - - return lastFocusedWidget; -} - -/** - * Gets `URI` of a instructions file open in an active editor instance, if any. - */ -export const getActiveInstructionsFileUri = ( - accessor: ServicesAccessor, -): URI | undefined => { - const codeEditorService = accessor.get(ICodeEditorService); - const model = codeEditorService.getActiveCodeEditor()?.getModel(); - if (model?.getLanguageId() === INSTRUCTIONS_LANGUAGE_ID) { - return model.uri; - } - return undefined; -}; - -/** - * Register the "Attach Instructions" command with its keybinding. - */ -KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: INSTRUCTIONS_COMMAND_ID, - weight: KeybindingWeight.WorkbenchContrib, - primary: INSTRUCTIONS_COMMAND_KEY_BINDING, - handler: command, - when: ContextKeyExpr.and(PromptsConfig.enabledCtx, ChatContextKeys.enabled), -}); - -/** - * Register the "Use Instructions" command in the `command palette`. - */ -MenuRegistry.appendMenuItem(MenuId.CommandPalette, { - command: { - id: INSTRUCTIONS_COMMAND_ID, - title: localize('attach-instructions.capitalized.ellipses', "Attach Instructions..."), - category: CHAT_CATEGORY - }, - when: ContextKeyExpr.and(PromptsConfig.enabledCtx, ChatContextKeys.enabled) -}); diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/createPromptCommand.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/createPromptCommand.ts index 9908f01032f..1d89c204d14 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/createPromptCommand.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/createPromptCommand.ts @@ -8,195 +8,170 @@ import { URI } from '../../../../../../../base/common/uri.js'; import { getCodeEditor } from '../../../../../../../editor/browser/editorBrowser.js'; import { SnippetController2 } from '../../../../../../../editor/contrib/snippet/browser/snippetController2.js'; import { localize } from '../../../../../../../nls.js'; -import { MenuId, MenuRegistry } from '../../../../../../../platform/actions/common/actions.js'; +import { Action2, MenuId, registerAction2 } from '../../../../../../../platform/actions/common/actions.js'; import { ICommandService } from '../../../../../../../platform/commands/common/commands.js'; import { ContextKeyExpr } from '../../../../../../../platform/contextkey/common/contextkey.js'; import { IFileService } from '../../../../../../../platform/files/common/files.js'; -import { ServicesAccessor } from '../../../../../../../platform/instantiation/common/instantiation.js'; -import { KeybindingsRegistry, KeybindingWeight } from '../../../../../../../platform/keybinding/common/keybindingsRegistry.js'; -import { ILabelService } from '../../../../../../../platform/label/common/label.js'; +import { IInstantiationService, ServicesAccessor } from '../../../../../../../platform/instantiation/common/instantiation.js'; +import { KeybindingWeight } from '../../../../../../../platform/keybinding/common/keybindingsRegistry.js'; import { ILogService } from '../../../../../../../platform/log/common/log.js'; import { INotificationService, NeverShowAgainScope, Severity } from '../../../../../../../platform/notification/common/notification.js'; import { IOpenerService } from '../../../../../../../platform/opener/common/opener.js'; import { PromptsConfig } from '../../../../../../../platform/prompts/common/config.js'; -import { IQuickInputService } from '../../../../../../../platform/quickinput/common/quickInput.js'; import { IUserDataSyncEnablementService, SyncResource } from '../../../../../../../platform/userDataSync/common/userDataSync.js'; -import { IWorkspaceContextService } from '../../../../../../../platform/workspace/common/workspace.js'; import { IEditorService } from '../../../../../../services/editor/common/editorService.js'; import { CONFIGURE_SYNC_COMMAND_ID } from '../../../../../../services/userDataSync/common/userDataSync.js'; import { ISnippetsService } from '../../../../../snippets/browser/snippets.js'; import { ChatContextKeys } from '../../../../common/chatContextKeys.js'; import { INSTRUCTIONS_LANGUAGE_ID, PROMPT_LANGUAGE_ID } from '../../../../common/promptSyntax/constants.js'; -import { IPromptsService, TPromptsType } from '../../../../common/promptSyntax/service/types.js'; +import { TPromptsType } from '../../../../common/promptSyntax/service/types.js'; import { CHAT_CATEGORY } from '../../../actions/chatActions.js'; import { askForPromptFileName } from './dialogs/askForPromptName.js'; import { askForPromptSourceFolder } from './dialogs/askForPromptSourceFolder.js'; import { createPromptFile } from './utils/createPromptFile.js'; -/** - * The command implementation. - */ -const command = async ( - accessor: ServicesAccessor, - type: TPromptsType, -): Promise => { +class AbstractNewPromptOrInstructionsFileAction extends Action2 { - const logService = accessor.get(ILogService); - const fileService = accessor.get(IFileService); - const labelService = accessor.get(ILabelService); - const openerService = accessor.get(IOpenerService); - const promptsService = accessor.get(IPromptsService); - const commandService = accessor.get(ICommandService); - const quickInputService = accessor.get(IQuickInputService); - const notificationService = accessor.get(INotificationService); - const workspaceService = accessor.get(IWorkspaceContextService); - const userDataSyncEnablementService = accessor.get(IUserDataSyncEnablementService); - const snippetService = accessor.get(ISnippetsService); - const editorService = accessor.get(IEditorService); - - - const placeHolder = (type === 'instructions') - ? localize( - 'workbench.command.instructions.create.location.placeholder', - "Select a location to create the instructions file in...", - ) - : localize( - 'workbench.command.prompt.create.location.placeholder', - "Select a location to create the prompt file in...", - ); - - const selectedFolder = await askForPromptSourceFolder({ - type, - placeHolder, - labelService, - openerService, - promptsService, - workspaceService, - quickInputService, - }); - - if (!selectedFolder) { - return; - } - - const fileName = await askForPromptFileName(type, selectedFolder.uri, quickInputService, fileService); - if (!fileName) { - return; - } - - const promptUri = await createPromptFile({ - fileName, - folder: selectedFolder.uri, - content: '', - fileService, - openerService, - }); - - await openerService.open(promptUri); - - const editor = getCodeEditor(editorService.activeTextEditorControl); - if (editor && editor.hasModel() && isEqual(editor.getModel().uri, promptUri)) { - const languageId = type === 'instructions' ? INSTRUCTIONS_LANGUAGE_ID : PROMPT_LANGUAGE_ID; - - const snippets = await snippetService.getSnippets(languageId, { fileTemplateSnippets: true, noRecencySort: true, includeNoPrefixSnippets: true }); - if (snippets.length > 0) { - SnippetController2.get(editor)?.apply([{ - range: editor.getModel().getFullModelRange(), - template: snippets[0].body - }]); - } - } - - if (selectedFolder.storage !== 'user') { - return; - } - - // due to PII concerns, synchronization of the 'user' reusable prompts - // is disabled by default, but we want to make that fact clear to the user - // hence after a 'user' prompt is create, we check if the synchronization - // was explicitly configured before, and if it wasn't, we show a suggestion - // to enable the synchronization logic in the Settings Sync configuration - - const isConfigured = userDataSyncEnablementService - .isResourceEnablementConfigured(SyncResource.Prompts); - const isSettingsSyncEnabled = userDataSyncEnablementService.isEnabled(); - - // if prompts synchronization has already been configured before or - // if settings sync service is currently disabled, nothing to do - if ((isConfigured === true) || (isSettingsSyncEnabled === false)) { - return; - } - - // show suggestion to enable synchronization of the user prompts and instructions to the user - notificationService.prompt( - Severity.Info, - localize( - 'workbench.command.prompts.create.user.enable-sync-notification', - "Do you want to backup and sync your user prompt and instruction files with Setting Sync?'", - ), - [ - { - label: localize('enable.capitalized', "Enable"), - run: () => { - commandService.executeCommand(CONFIGURE_SYNC_COMMAND_ID) - .catch((error) => { - logService.error(`Failed to run '${CONFIGURE_SYNC_COMMAND_ID}' command: ${error}.`); - }); - }, - }, - { - label: localize('learnMore.capitalized', "Learn More"), - run: () => { - openerService.open(URI.parse('https://aka.ms/vscode-settings-sync-help')); - }, - }, - ], - { - neverShowAgain: { - id: 'workbench.command.prompts.create.user.enable-sync-notification', - scope: NeverShowAgainScope.PROFILE, - }, - }, - ); -}; - -function register(type: TPromptsType, id: string, title: string) { - /** - * Register the command. - */ - KeybindingsRegistry.registerCommandAndKeybindingRule({ - id, - weight: KeybindingWeight.WorkbenchContrib, - handler: async (accessor: ServicesAccessor): Promise => { - return command(accessor, type); - }, - when: ContextKeyExpr.and(PromptsConfig.enabledCtx, ChatContextKeys.enabled), - }); - - /** - * Register the command in the command palette. - */ - MenuRegistry.appendMenuItem(MenuId.CommandPalette, { - command: { + constructor(id: string, title: string, private readonly type: TPromptsType) { + super({ id, title, - category: CHAT_CATEGORY - }, - when: ContextKeyExpr.and(PromptsConfig.enabledCtx, ChatContextKeys.enabled) - }); + f1: false, + precondition: ContextKeyExpr.and(PromptsConfig.enabledCtx, ChatContextKeys.enabled), + category: CHAT_CATEGORY, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib + }, + menu: { + id: MenuId.CommandPalette, + when: ContextKeyExpr.and(PromptsConfig.enabledCtx, ChatContextKeys.enabled) + } + }); + } + + public override async run(accessor: ServicesAccessor) { + const logService = accessor.get(ILogService); + const openerService = accessor.get(IOpenerService); + const commandService = accessor.get(ICommandService); + const notificationService = accessor.get(INotificationService); + const userDataSyncEnablementService = accessor.get(IUserDataSyncEnablementService); + const snippetService = accessor.get(ISnippetsService); + const editorService = accessor.get(IEditorService); + const fileService = accessor.get(IFileService); + const instaService = accessor.get(IInstantiationService); + + const placeHolder = (this.type === 'instructions') + ? localize( + 'workbench.command.instructions.create.location.placeholder', + "Select a location to create the instructions file in...", + ) + : localize( + 'workbench.command.prompt.create.location.placeholder', + "Select a location to create the prompt file in...", + ); + + const selectedFolder = await instaService.invokeFunction(askForPromptSourceFolder, this.type, placeHolder); + if (!selectedFolder) { + return; + } + + const fileName = await instaService.invokeFunction(askForPromptFileName, this.type, selectedFolder.uri); + if (!fileName) { + return; + } + + const promptUri = await createPromptFile(fileService, { + fileName, + folder: selectedFolder.uri, + content: '' + }); + + await openerService.open(promptUri); + + const editor = getCodeEditor(editorService.activeTextEditorControl); + if (editor && editor.hasModel() && isEqual(editor.getModel().uri, promptUri)) { + const languageId = this.type === 'instructions' ? INSTRUCTIONS_LANGUAGE_ID : PROMPT_LANGUAGE_ID; + + const snippets = await snippetService.getSnippets(languageId, { fileTemplateSnippets: true, noRecencySort: true, includeNoPrefixSnippets: true }); + if (snippets.length > 0) { + SnippetController2.get(editor)?.apply([{ + range: editor.getModel().getFullModelRange(), + template: snippets[0].body + }]); + } + } + + if (selectedFolder.storage !== 'user') { + return; + } + + // due to PII concerns, synchronization of the 'user' reusable prompts + // is disabled by default, but we want to make that fact clear to the user + // hence after a 'user' prompt is create, we check if the synchronization + // was explicitly configured before, and if it wasn't, we show a suggestion + // to enable the synchronization logic in the Settings Sync configuration + + const isConfigured = userDataSyncEnablementService + .isResourceEnablementConfigured(SyncResource.Prompts); + const isSettingsSyncEnabled = userDataSyncEnablementService.isEnabled(); + + // if prompts synchronization has already been configured before or + // if settings sync service is currently disabled, nothing to do + if ((isConfigured === true) || (isSettingsSyncEnabled === false)) { + return; + } + + // show suggestion to enable synchronization of the user prompts and instructions to the user + notificationService.prompt( + Severity.Info, + localize( + 'workbench.command.prompts.create.user.enable-sync-notification', + "Do you want to backup and sync your user prompt and instruction files with Setting Sync?'", + ), + [ + { + label: localize('enable.capitalized', "Enable"), + run: () => { + commandService.executeCommand(CONFIGURE_SYNC_COMMAND_ID) + .catch((error) => { + logService.error(`Failed to run '${CONFIGURE_SYNC_COMMAND_ID}' command: ${error}.`); + }); + }, + }, + { + label: localize('learnMore.capitalized', "Learn More"), + run: () => { + openerService.open(URI.parse('https://aka.ms/vscode-settings-sync-help')); + }, + }, + ], + { + neverShowAgain: { + id: 'workbench.command.prompts.create.user.enable-sync-notification', + scope: NeverShowAgainScope.PROFILE, + }, + }, + ); + } } + export const NEW_PROMPT_COMMAND_ID = 'workbench.command.new.prompt'; export const NEW_INSTRUCTIONS_COMMAND_ID = 'workbench.command.new.instructions'; -register( - 'instructions', - NEW_INSTRUCTIONS_COMMAND_ID, - localize('commands.new.instructions.local.title', "New Instructions File...") -); -register( - 'prompt', - NEW_PROMPT_COMMAND_ID, - localize('commands.new.prompt.local.title', "New Prompt File...") -); +class NewPromptFileAction extends AbstractNewPromptOrInstructionsFileAction { + constructor() { + super(NEW_PROMPT_COMMAND_ID, localize('commands.new.prompt.local.title', "New Prompt File..."), 'prompt'); + } +} + +class NewInstructionsFileAction extends AbstractNewPromptOrInstructionsFileAction { + constructor() { + super(NEW_INSTRUCTIONS_COMMAND_ID, localize('commands.new.instructions.local.title', "New Instructions File..."), 'instructions'); + } +} + +registerAction2(NewPromptFileAction); +registerAction2(NewInstructionsFileAction); + diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/dialogs/askForPromptName.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/dialogs/askForPromptName.ts index 2ac54236cf9..446a5e36b11 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/dialogs/askForPromptName.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/dialogs/askForPromptName.ts @@ -11,16 +11,19 @@ import { URI } from '../../../../../../../../base/common/uri.js'; import { IFileService } from '../../../../../../../../platform/files/common/files.js'; import Severity from '../../../../../../../../base/common/severity.js'; import { isValidBasename } from '../../../../../../../../base/common/extpath.js'; +import { ServicesAccessor } from '../../../../../../../../editor/browser/editorExtensions.js'; /** * Asks the user for a file name. */ -export const askForPromptFileName = async ( +export async function askForPromptFileName( + accessor: ServicesAccessor, type: TPromptsType, - selectedFolder: URI, - quickInputService: IQuickInputService, - fileService: IFileService, -): Promise => { + selectedFolder: URI +): Promise { + const quickInputService = accessor.get(IQuickInputService); + const fileService = accessor.get(IFileService); + const placeHolder = (type === 'instructions') ? localize('askForInstructionsFileName.placeholder', "Enter the name of the instructions file") : localize('askForPromptFileName.placeholder', "Enter the name of the prompt file"); @@ -71,4 +74,4 @@ export const askForPromptFileName = async ( } return sanitizeInput(result); -}; +} diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/dialogs/askForPromptSourceFolder.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/dialogs/askForPromptSourceFolder.ts index bf71ed3e107..fd5762c7e2b 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/dialogs/askForPromptSourceFolder.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/dialogs/askForPromptSourceFolder.ts @@ -13,21 +13,7 @@ import { PROMPT_DOCUMENTATION_URL } from '../../../../../common/promptSyntax/con import { IWorkspaceContextService } from '../../../../../../../../platform/workspace/common/workspace.js'; import { IPromptPath, IPromptsService, TPromptsType } from '../../../../../common/promptSyntax/service/types.js'; import { IPickOptions, IQuickInputService, IQuickPickItem } from '../../../../../../../../platform/quickinput/common/quickInput.js'; - -/** - * Options for {@link askForPromptSourceFolder} dialog. - */ -interface IAskForFolderOptions { - - readonly type: TPromptsType; - readonly placeHolder: string; - - readonly labelService: ILabelService; - readonly openerService: IOpenerService; - readonly promptsService: IPromptsService; - readonly quickInputService: IQuickInputService; - readonly workspaceService: IWorkspaceContextService; -} +import { ServicesAccessor } from '../../../../../../../../platform/instantiation/common/instantiation.js'; interface IFolderQuickPickItem extends IQuickPickItem { readonly folder: IPromptPath; @@ -37,10 +23,15 @@ interface IFolderQuickPickItem extends IQuickPickItem { * Asks the user for a specific prompt folder, if multiple folders provided. * Returns immediately if only one folder available. */ -export const askForPromptSourceFolder = async ( - options: IAskForFolderOptions, -): Promise => { - const { type, placeHolder, promptsService, quickInputService, labelService, openerService, workspaceService } = options; +export async function askForPromptSourceFolder( + accessor: ServicesAccessor, + type: TPromptsType, + placeHolder: string +): Promise { + const quickInputService = accessor.get(IQuickInputService); + const promptsService = accessor.get(IPromptsService); + const labelService = accessor.get(ILabelService); + const workspaceService = accessor.get(IWorkspaceContextService); // get prompts source folders based on the prompt type const folders = promptsService.getSourceFolders(type); @@ -49,7 +40,8 @@ export const askForPromptSourceFolder = async ( // note! this is a temporary solution and must be replaced with a dialog to select // a custom folder path, or switch to a different prompt type if (folders.length === 0) { - return await showNoFoldersDialog(quickInputService, openerService); + await showNoFoldersDialog(accessor); + return; } // if there is only one folder, no need to ask @@ -118,7 +110,7 @@ export const askForPromptSourceFolder = async ( } return answer.folder; -}; +} /** * Shows a dialog to the user when no prompt source folders are found. @@ -126,10 +118,10 @@ export const askForPromptSourceFolder = async ( * Note! this is a temporary solution and must be replaced with a dialog to select * a custom folder path, or switch to a different prompt type */ -const showNoFoldersDialog = async ( - quickInputService: IQuickInputService, - openerService: IOpenerService, -): Promise => { +async function showNoFoldersDialog(accessor: ServicesAccessor): Promise { + const quickInputService = accessor.get(IQuickInputService); + const openerService = accessor.get(IOpenerService); + const docsQuickPick: WithUriValue = { type: 'item', label: localize( @@ -151,11 +143,7 @@ const showNoFoldersDialog = async ( canPickMany: false, }); - if (!result) { - return; + if (result) { + await openerService.open(result.value); } - - await openerService.open(result.value); - - return; -}; +} diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/utils/createPromptFile.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/utils/createPromptFile.ts index dbea375a77d..cc3fff787c6 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/utils/createPromptFile.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/utils/createPromptFile.ts @@ -9,7 +9,6 @@ import { assert } from '../../../../../../../../base/common/assert.js'; import { VSBuffer } from '../../../../../../../../base/common/buffer.js'; import { dirname } from '../../../../../../../../base/common/resources.js'; import { IFileService } from '../../../../../../../../platform/files/common/files.js'; -import { IOpenerService } from '../../../../../../../../platform/opener/common/opener.js'; import { isPromptOrInstructionsFile, PROMPT_FILE_EXTENSION } from '../../../../../../../../platform/prompts/common/constants.js'; /** @@ -31,9 +30,6 @@ interface ICreatePromptFileOptions { * Initial contents of the prompt file. */ readonly content: string; - - fileService: IFileService; - openerService: IOpenerService; } /** @@ -44,10 +40,11 @@ interface ICreatePromptFileOptions { * - if the `fileName` does not end with {@link PROMPT_FILE_EXTENSION} * - if a folder or file with the same already name exists in the destination folder */ -export const createPromptFile = async ( +export async function createPromptFile( + fileService: IFileService, options: ICreatePromptFileOptions, -): Promise => { - const { fileName, folder, content, fileService, openerService } = options; +): Promise { + const { fileName, folder, content } = options; const promptUri = URI.joinPath(folder, fileName); @@ -66,9 +63,6 @@ export const createPromptFile = async ( new FolderExists(promptUri.fsPath), ); - // prompt file already exists so open it - await openerService.open(promptUri); - return promptUri; } @@ -79,4 +73,4 @@ export const createPromptFile = async ( await fileService.createFile(promptUri, VSBuffer.fromString(content)); return promptUri; -}; +} diff --git a/src/vs/workbench/contrib/chat/common/chatAgents.ts b/src/vs/workbench/contrib/chat/common/chatAgents.ts index 4237022ac05..440f15c08a1 100644 --- a/src/vs/workbench/contrib/chat/common/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/chatAgents.ts @@ -15,7 +15,7 @@ import { IObservable, observableValue } from '../../../../base/common/observable import { equalsIgnoreCase } from '../../../../base/common/strings.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { URI } from '../../../../base/common/uri.js'; -import { Command, ProviderResult } from '../../../../editor/common/languages.js'; +import { Command } from '../../../../editor/common/languages.js'; import { ContextKeyExpr, IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; @@ -73,7 +73,6 @@ export interface IChatAgentImplementation { setRequestPaused?(requestId: string, isPaused: boolean): void; provideFollowups?(request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise; provideChatTitle?: (history: IChatAgentHistoryEntry[], token: CancellationToken) => Promise; - provideSampleQuestions?(location: ChatAgentLocation, token: CancellationToken): ProviderResult; } export interface IChatParticipantDetectionResult { @@ -342,7 +341,7 @@ export class ChatAgentService extends Disposable implements IChatAgentService { this._onDidChangeAgents.fire(undefined); if (entry.data.isDefault) { - this._hasDefaultAgent.set(false); + this._hasDefaultAgent.set(Iterable.some(this._agents.values(), agent => agent.data.isDefault)); } }); } @@ -497,11 +496,7 @@ export class ChatAgentService extends Disposable implements IChatAgentService { async getChatTitle(id: string, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { const data = this._agents.get(id); - if (!data?.impl) { - throw new Error(`No activated agent with id "${id}"`); - } - - if (!data.impl?.provideChatTitle) { + if (!data?.impl?.provideChatTitle) { return undefined; } @@ -604,14 +599,6 @@ export class MergedChatAgent implements IChatAgent { return []; } - provideSampleQuestions(location: ChatAgentLocation, token: CancellationToken): ProviderResult { - if (this.impl.provideSampleQuestions) { - return this.impl.provideSampleQuestions(location, token); - } - - return undefined; - } - toJSON(): IChatAgentData { return this.data; } diff --git a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts index 9e7c2e5d52c..dcdf6e52341 100644 --- a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts +++ b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts @@ -57,13 +57,13 @@ export namespace ChatContextKeys { }; export const Entitlement = { - signedOut: new RawContextKey('chatSetupSignedOut', false, true), // True when user is signed out. - canSignUp: new RawContextKey('chatPlanCanSignUp', false, true), // True when user can sign up to be a chat limited user. - limited: new RawContextKey('chatPlanLimited', false, true), // True when user is a chat limited user. - pro: new RawContextKey('chatPlanPro', false, true), // True when user is a chat pro user. - proPlus: new RawContextKey('chatPlanProPlus', false, true), // True when user is a chat pro plus user. - business: new RawContextKey('chatPlanBusiness', false, true), // True when user is a chat business user. - enterprise: new RawContextKey('chatPlanEnterprise', false, true) // True when user is a chat enterprise user. + signedOut: new RawContextKey('chatEntitlementSignedOut', false, true), // True when user is signed out. + canSignUp: new RawContextKey('chatPlanCanSignUp', false, true), // True when user can sign up to be a chat limited user. + limited: new RawContextKey('chatPlanLimited', false, true), // True when user is a chat limited user. + pro: new RawContextKey('chatPlanPro', false, true), // True when user is a chat pro user. + proPlus: new RawContextKey('chatPlanProPlus', false, true), // True when user is a chat pro plus user. + business: new RawContextKey('chatPlanBusiness', false, true), // True when user is a chat business user. + enterprise: new RawContextKey('chatPlanEnterprise', false, true) // True when user is a chat enterprise user. }; export const chatQuotaExceeded = new RawContextKey('chatQuotaExceeded', false, true); diff --git a/src/vs/workbench/contrib/chat/common/chatEntitlementService.ts b/src/vs/workbench/contrib/chat/common/chatEntitlementService.ts index a4986bf0be3..7ee94ca50de 100644 --- a/src/vs/workbench/contrib/chat/common/chatEntitlementService.ts +++ b/src/vs/workbench/contrib/chat/common/chatEntitlementService.ts @@ -56,13 +56,28 @@ export enum ChatEntitlement { Enterprise } -export enum ChatSentiment { - /** Out of the box value */ - Standard = 1, - /** Explicitly disabled/hidden by user */ - Disabled = 2, - /** Extensions installed */ - Installed = 3 +export interface IChatSentiment { + + /** + * User has Chat installed. + */ + installed?: boolean; + + /** + * User signals no intent in using Chat. + * + * Note: in contrast to `disabled`, this should not only disable + * Chat but also hide all of its UI. + */ + hidden?: boolean; + + /** + * User signals intent to disable Chat. + * + * Note: in contrast to `hidden`, this should not hide + * Chat but but disable its functionality. + */ + disabled?: boolean; } export interface IChatEntitlementService { @@ -82,7 +97,7 @@ export interface IChatEntitlementService { readonly onDidChangeSentiment: Event; - readonly sentiment: ChatSentiment; + readonly sentiment: IChatSentiment; } //#region Helper Functions @@ -156,6 +171,7 @@ export class ChatEntitlementService extends Disposable implements IChatEntitleme Event.filter( this.contextKeyService.onDidChangeContext, e => e.affectsSome(new Set([ ChatContextKeys.Setup.hidden.key, + ChatContextKeys.Setup.disabled.key, ChatContextKeys.Setup.installed.key ])), this._store ), () => { }, this._store @@ -280,14 +296,12 @@ export class ChatEntitlementService extends Disposable implements IChatEntitleme readonly onDidChangeSentiment: Event; - get sentiment(): ChatSentiment { - if (this.contextKeyService.getContextKeyValue(ChatContextKeys.Setup.installed.key) === true) { - return ChatSentiment.Installed; - } else if (this.contextKeyService.getContextKeyValue(ChatContextKeys.Setup.hidden.key) === true) { - return ChatSentiment.Disabled; - } - - return ChatSentiment.Standard; + get sentiment(): IChatSentiment { + return { + installed: this.contextKeyService.getContextKeyValue(ChatContextKeys.Setup.installed.key) === true, + hidden: this.contextKeyService.getContextKeyValue(ChatContextKeys.Setup.hidden.key) === true, + disabled: this.contextKeyService.getContextKeyValue(ChatContextKeys.Setup.disabled.key) === true + }; } //#endregion @@ -438,8 +452,8 @@ export class ChatEntitlementRequests extends Disposable { })); this._register(this.context.onDidChange(() => { - if (!this.context.state.installed || this.context.state.entitlement === ChatEntitlement.Unknown) { - // When the extension is not installed or the user is not entitled + if (!this.context.state.installed || this.context.state.disabled || this.context.state.entitlement === ChatEntitlement.Unknown) { + // When the extension is not installed, disabled or the user is not entitled // make sure to clear quotas so that any indicators are also gone this.state = { entitlement: this.state.entitlement, quotas: undefined }; this.chatQuotasAccessor.clearQuotas(); @@ -530,8 +544,8 @@ export class ChatEntitlementRequests extends Disposable { private async doResolveEntitlement(session: AuthenticationSession, token: CancellationToken): Promise { if (ChatEntitlementRequests.providerId(this.configurationService) === defaultChat.enterpriseProviderId) { - this.logService.trace('[chat entitlement]: enterprise provider, assuming Pro'); - return { entitlement: ChatEntitlement.Pro }; + this.logService.trace('[chat entitlement]: enterprise provider, assuming Enterprise plan'); + return { entitlement: ChatEntitlement.Enterprise }; } if (token.isCancellationRequested) { @@ -627,7 +641,7 @@ export class ChatEntitlementRequests extends Disposable { if (response.monthly_quotas?.chat && typeof response.limited_user_quotas?.chat === 'number') { quotas.chat = { total: response.monthly_quotas.chat, - percentRemaining: (response.limited_user_quotas.chat / response.monthly_quotas.chat) * 100, + percentRemaining: Math.min(100, Math.max(0, (response.limited_user_quotas.chat / response.monthly_quotas.chat) * 100)), overageEnabled: false, overageCount: 0, unlimited: false @@ -637,7 +651,7 @@ export class ChatEntitlementRequests extends Disposable { if (response.monthly_quotas?.completions && typeof response.limited_user_quotas?.completions === 'number') { quotas.completions = { total: response.monthly_quotas.completions, - percentRemaining: (response.limited_user_quotas.completions / response.monthly_quotas.completions) * 100, + percentRemaining: Math.min(100, Math.max(0, (response.limited_user_quotas.completions / response.monthly_quotas.completions) * 100)), overageEnabled: false, overageCount: 0, unlimited: false @@ -653,7 +667,7 @@ export class ChatEntitlementRequests extends Disposable { } const quotaSnapshot: IQuotaSnapshot = { total: rawQuotaSnapshot.entitlement, - percentRemaining: rawQuotaSnapshot.percent_remaining, + percentRemaining: Math.min(100, Math.max(0, rawQuotaSnapshot.percent_remaining)), overageEnabled: rawQuotaSnapshot.overage_permitted, overageCount: rawQuotaSnapshot.overage_count, unlimited: rawQuotaSnapshot.unlimited @@ -841,11 +855,16 @@ export class ChatEntitlementRequests extends Disposable { //#region Context -export interface IChatEntitlementContextState { +export interface IChatEntitlementContextState extends IChatSentiment { + + /** + * Users last known or resolved entitlement. + */ entitlement: ChatEntitlement; - hidden?: boolean; - installed?: boolean; - disabled?: boolean; + + /** + * User is or was a registered Chat user. + */ registered?: boolean; } @@ -855,11 +874,13 @@ export class ChatEntitlementContext extends Disposable { private readonly canSignUpContextKey: IContextKey; private readonly signedOutContextKey: IContextKey; + private readonly limitedContextKey: IContextKey; private readonly proContextKey: IContextKey; private readonly proPlusContextKey: IContextKey; private readonly businessContextKey: IContextKey; private readonly enterpriseContextKey: IContextKey; + private readonly hiddenContext: IContextKey; private readonly installedContext: IContextKey; private readonly disabledContext: IContextKey; @@ -913,11 +934,10 @@ export class ChatEntitlementContext extends Disposable { } const defaultChatExtension = this.extensionsWorkbenchService.local.find(value => ExtensionIdentifier.equals(value.identifier.id, defaultChat.extensionId)); - this.update({ - // TODO@bpasero considering enablement state here as well for historic reasons, should revisit when Copilot can be enabled/disabled more generally - installed: !!defaultChatExtension?.local && this.extensionEnablementService.isEnabled(defaultChatExtension.local), - disabled: !!defaultChatExtension?.local && !this.extensionEnablementService.isEnabled(defaultChatExtension.local) - }); + const installed = !!defaultChatExtension?.local; + const disabled = installed && !this.extensionEnablementService.isEnabled(defaultChatExtension.local); + + this.update({ installed, disabled }); })); } @@ -931,8 +951,8 @@ export class ChatEntitlementContext extends Disposable { this._state.installed = context.installed; this._state.disabled = context.disabled; - if (context.installed) { - context.hidden = false; // allows to fallback if the extension is uninstalled + if (context.installed && !context.disabled) { + context.hidden = false; // treat this as a sign to make Chat visible again in case it is hidden } } diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index bdde10e3a13..a713a2fb4c0 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { asArray } from '../../../../base/common/arrays.js'; -import { DeferredPromise } from '../../../../base/common/async.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { IMarkdownString, MarkdownString, isMarkdownString } from '../../../../base/common/htmlContent.js'; @@ -13,13 +12,13 @@ import { ResourceMap } from '../../../../base/common/map.js'; import { revive } from '../../../../base/common/marshalling.js'; import { Schemas } from '../../../../base/common/network.js'; import { equals } from '../../../../base/common/objects.js'; -import { IObservable, ITransaction, observableFromEvent, ObservablePromise, observableSignalFromEvent, observableValue } from '../../../../base/common/observable.js'; +import { IObservable, ITransaction, ObservablePromise, observableFromEvent, observableSignalFromEvent, observableValue } from '../../../../base/common/observable.js'; import { basename, isEqual } from '../../../../base/common/resources.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { URI, UriComponents, UriDto, isUriComponents } from '../../../../base/common/uri.js'; import { generateUuid } from '../../../../base/common/uuid.js'; -import { IOffsetRange, OffsetRange } from '../../../../editor/common/core/offsetRange.js'; import { IRange } from '../../../../editor/common/core/range.js'; +import { IOffsetRange, OffsetRange } from '../../../../editor/common/core/ranges/offsetRange.js'; import { Location, SymbolKind, TextEdit } from '../../../../editor/common/languages.js'; import { localize } from '../../../../nls.js'; import { ILogService } from '../../../../platform/log/common/log.js'; @@ -28,7 +27,7 @@ import { CellUri, ICellEditOperation } from '../../notebook/common/notebookCommo import { IChatAgentCommand, IChatAgentData, IChatAgentResult, IChatAgentService, reviveSerializedAgent } from './chatAgents.js'; import { IChatEditingService, IChatEditingSession } from './chatEditingService.js'; import { ChatRequestTextPart, IParsedChatRequest, reviveParsedChatRequest } from './chatParserTypes.js'; -import { ChatAgentVoteDirection, ChatAgentVoteDownReason, IChatAgentMarkdownContentWithVulnerability, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatEditingSessionAction, IChatExtensionsContent, IChatFollowup, IChatLocationData, IChatMarkdownContent, IChatNotebookEdit, IChatProgress, IChatProgressMessage, IChatResponseCodeblockUriPart, IChatResponseProgressFileTreeData, IChatTask, IChatTextEdit, IChatToolInvocation, IChatToolInvocationSerialized, IChatTreeData, IChatUndoStop, IChatUsedContext, IChatWarningMessage, isIUsedContext } from './chatService.js'; +import { ChatAgentVoteDirection, ChatAgentVoteDownReason, IChatAgentMarkdownContentWithVulnerability, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatEditingSessionAction, IChatExtensionsContent, IChatFollowup, IChatLocationData, IChatMarkdownContent, IChatNotebookEdit, IChatPrepareToolInvocationPart, IChatProgress, IChatProgressMessage, IChatResponseCodeblockUriPart, IChatResponseProgressFileTreeData, IChatTask, IChatTaskSerialized, IChatTextEdit, IChatToolInvocation, IChatToolInvocationSerialized, IChatTreeData, IChatUndoStop, IChatUsedContext, IChatWarningMessage, isIUsedContext } from './chatService.js'; import { IChatRequestVariableValue } from './chatVariables.js'; import { ChatAgentLocation, ChatMode } from './constants.js'; @@ -200,9 +199,14 @@ export interface IElementVariableEntry extends IBaseChatRequestVariableEntry { readonly kind: 'element'; } +export interface IPromptFileVariableEntry extends IBaseChatRequestVariableEntry { + readonly kind: 'promptFile'; +} + export type IChatRequestVariableEntry = IGenericChatRequestVariableEntry | IChatRequestImplicitVariableEntry | IChatRequestPasteVariableEntry | ISymbolVariableEntry | ICommandResultVariableEntry | IDiagnosticVariableEntry | IImageVariableEntry | IChatRequestToolEntry - | IChatRequestDirectoryEntry | IChatRequestFileEntry | INotebookOutputVariableEntry | IElementVariableEntry; + | IChatRequestDirectoryEntry | IChatRequestFileEntry | INotebookOutputVariableEntry | IElementVariableEntry + | IPromptFileVariableEntry; export function isImplicitVariableEntry(obj: IChatRequestVariableEntry): obj is IChatRequestImplicitVariableEntry { return obj.kind === 'implicit'; @@ -307,6 +311,7 @@ export type IChatProgressHistoryResponseContent = | IChatCommandButton | IChatWarningMessage | IChatTask + | IChatTaskSerialized | IChatTextEditGroup | IChatNotebookEditGroup | IChatConfirmation @@ -319,9 +324,10 @@ export type IChatProgressResponseContent = | IChatProgressHistoryResponseContent | IChatToolInvocation | IChatToolInvocationSerialized - | IChatUndoStop; + | IChatUndoStop + | IChatPrepareToolInvocationPart; -const nonHistoryKinds = new Set(['toolInvocation', 'toolInvocationSerialized']); +const nonHistoryKinds = new Set(['toolInvocation', 'toolInvocationSerialized', 'undoStop', 'prepareToolInvocation']); function isChatProgressHistoryResponseContent(content: IChatProgressResponseContent): content is IChatProgressHistoryResponseContent { return !nonHistoryKinds.has(content.kind); } @@ -544,6 +550,7 @@ class AbstractResponse implements IResponse { case 'toolInvocationSerialized': case 'extensions': case 'undoStop': + case 'prepareToolInvocation': // Ignore continue; case 'inlineReference': @@ -1068,10 +1075,8 @@ export interface IChatModel { readonly onDidDispose: Event; readonly onDidChange: Event; readonly sessionId: string; - readonly initState: ChatModelInitState; readonly initialLocation: ChatAgentLocation; readonly title: string; - readonly sampleQuestions: IChatFollowup[] | undefined; readonly requestInProgress: boolean; readonly requestInProgressObs: IObservable; readonly requestPausibility: ChatPauseState; @@ -1314,12 +1319,6 @@ export interface IChatInitEvent { kind: 'initialize'; } -export enum ChatModelInitState { - Created, - Initializing, - Initialized -} - export class ChatModel extends Disposable implements IChatModel { static getDefaultTitle(requests: (ISerializableChatRequestData | IChatRequestModel)[]): string { const firstRequestMessage = requests.at(0)?.message ?? ''; @@ -1336,13 +1335,6 @@ export class ChatModel extends Disposable implements IChatModel { readonly onDidChange = this._onDidChange.event; private _requests: ChatRequestModel[]; - private _initState: ChatModelInitState = ChatModelInitState.Created; - private _isInitializedDeferred = new DeferredPromise(); - - private _sampleQuestions: IChatFollowup[] | undefined; - get sampleQuestions(): IChatFollowup[] | undefined { - return this._sampleQuestions; - } // TODO to be clear, this is not the same as the id from the session object, which belongs to the provider. // It's easier to be able to identify this model before its async initialization is complete @@ -1410,10 +1402,6 @@ export class ChatModel extends Disposable implements IChatModel { this._initialResponderAvatarIconUri; } - get initState(): ChatModelInitState { - return this._initState; - } - private _isImported = false; get isImported(): boolean { return this._isImported; @@ -1604,45 +1592,6 @@ export class ChatModel extends Disposable implements IChatModel { } } - startInitialize(): void { - if (this.initState !== ChatModelInitState.Created) { - throw new Error(`ChatModel is in the wrong state for startInitialize: ${ChatModelInitState[this.initState]}`); - } - this._initState = ChatModelInitState.Initializing; - } - - deinitialize(): void { - this._initState = ChatModelInitState.Created; - this._isInitializedDeferred = new DeferredPromise(); - } - - initialize(sampleQuestions?: IChatFollowup[]): void { - if (this.initState !== ChatModelInitState.Initializing) { - // Must call startInitialize before initialize, and only call it once - throw new Error(`ChatModel is in the wrong state for initialize: ${ChatModelInitState[this.initState]}`); - } - - this._initState = ChatModelInitState.Initialized; - this._sampleQuestions = sampleQuestions; - - this._isInitializedDeferred.complete(); - this._onDidChange.fire({ kind: 'initialize' }); - } - - setInitializationError(error: Error): void { - if (this.initState !== ChatModelInitState.Initializing) { - throw new Error(`ChatModel is in the wrong state for setInitializationError: ${ChatModelInitState[this.initState]}`); - } - - if (!this._isInitializedDeferred.isSettled) { - this._isInitializedDeferred.error(error); - } - } - - waitForInitialization(): Promise { - return this._isInitializedDeferred.p; - } - getRequests(): ChatRequestModel[] { return this._requests; } @@ -1739,23 +1688,8 @@ export class ChatModel extends Disposable implements IChatModel { throw new Error('acceptResponseProgress: Adding progress to a completed response'); } - if (progress.kind === 'markdownContent' || - progress.kind === 'treeData' || - progress.kind === 'inlineReference' || - progress.kind === 'codeblockUri' || - progress.kind === 'markdownVuln' || - progress.kind === 'progressMessage' || - progress.kind === 'command' || - progress.kind === 'textEdit' || - progress.kind === 'notebookEdit' || - progress.kind === 'warning' || - progress.kind === 'progressTask' || - progress.kind === 'confirmation' || - progress.kind === 'extensions' || - progress.kind === 'toolInvocation' - ) { - request.response.updateContent(progress, quiet); - } else if (progress.kind === 'usedContext' || progress.kind === 'reference') { + + if (progress.kind === 'usedContext' || progress.kind === 'reference') { request.response.applyReference(progress); } else if (progress.kind === 'codeCitation') { request.response.applyCodeCitation(progress); @@ -1763,8 +1697,11 @@ export class ChatModel extends Disposable implements IChatModel { this._onDidChange.fire({ kind: 'move', target: progress.uri, range: progress.range }); } else if (progress.kind === 'undoStop') { request.response.addUndoStop(progress); - } else { + } else if (progress.kind === 'progressTaskResult') { + // Should have been handled upstream, not sent to model this.logService.error(`Couldn't handle progress: ${JSON.stringify(progress)}`); + } else { + request.response.updateContent(progress, quiet); } } diff --git a/src/vs/workbench/contrib/chat/common/chatParserTypes.ts b/src/vs/workbench/contrib/chat/common/chatParserTypes.ts index be00a30a4dc..5d612849fd4 100644 --- a/src/vs/workbench/contrib/chat/common/chatParserTypes.ts +++ b/src/vs/workbench/contrib/chat/common/chatParserTypes.ts @@ -5,7 +5,7 @@ import { revive } from '../../../../base/common/marshalling.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; -import { IOffsetRange, OffsetRange } from '../../../../editor/common/core/offsetRange.js'; +import { IOffsetRange, OffsetRange } from '../../../../editor/common/core/ranges/offsetRange.js'; import { IRange } from '../../../../editor/common/core/range.js'; import { IChatAgentCommand, IChatAgentData, IChatAgentService, reviveSerializedAgent } from './chatAgents.js'; import { IChatRequestVariableEntry, IDiagnosticVariableEntryFilterData } from './chatModel.js'; diff --git a/src/vs/workbench/contrib/chat/common/chatRequestParser.ts b/src/vs/workbench/contrib/chat/common/chatRequestParser.ts index f47b7ddf412..d74c12c6f4f 100644 --- a/src/vs/workbench/contrib/chat/common/chatRequestParser.ts +++ b/src/vs/workbench/contrib/chat/common/chatRequestParser.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { OffsetRange } from '../../../../editor/common/core/offsetRange.js'; +import { OffsetRange } from '../../../../editor/common/core/ranges/offsetRange.js'; import { IPosition, Position } from '../../../../editor/common/core/position.js'; import { Range } from '../../../../editor/common/core/range.js'; import { IChatAgentData, IChatAgentService } from './chatAgents.js'; @@ -16,7 +16,7 @@ import { IPromptsService } from './promptSyntax/service/types.js'; const agentReg = /^@([\w_\-\.]+)(?=(\s|$|\b))/i; // An @-agent const variableReg = /^#([\w_\-]+)(:\d+)?(?=(\s|$|\b))/i; // A #-variable with an optional numeric : arg (@response:2) -const slashReg = /\/([\w_\-\.:]+)(?=(\s|$|\b))/i; // A / command +const slashReg = /^\/([\w_\-\.:]+)(?=(\s|$|\b))/i; // A / command export interface IChatParserContext { /** Used only as a disambiguator, when the query references an agent that has a duplicate with the same name. */ @@ -169,8 +169,16 @@ export class ChatRequestParser { return; } - if (parts.some(p => p instanceof ChatRequestSlashCommandPart)) { - // Only one slash command allowed + if (parts.some(p => !(p instanceof ChatRequestAgentPart) && !(p instanceof ChatRequestTextPart && p.text.trim() === ''))) { + // no other part than agent or non-whitespace text allowed: that also means no other slash command + return; + } + + // only whitespace after the last part + const previousPart = parts.at(-1); + const previousPartEnd = previousPart?.range.endExclusive ?? 0; + const textSincePreviousPart = fullMessage.slice(previousPartEnd, offset); + if (textSincePreviousPart.trim() !== '') { return; } @@ -180,18 +188,6 @@ export class ChatRequestParser { const usedAgent = parts.find((p): p is ChatRequestAgentPart => p instanceof ChatRequestAgentPart); if (usedAgent) { - // The slash command must come immediately after the agent - if (parts.some(p => (p instanceof ChatRequestTextPart && p.text.trim() !== '') || !(p instanceof ChatRequestAgentPart) && !(p instanceof ChatRequestTextPart))) { - return; - } - - const previousPart = parts.at(-1); - const previousPartEnd = previousPart?.range.endExclusive ?? 0; - const textSincePreviousPart = fullMessage.slice(previousPartEnd, offset); - if (textSincePreviousPart.trim() !== '') { - return; - } - const subCommand = usedAgent.agent.slashCommands.find(c => c.name === command); if (subCommand) { // Valid agent subcommand diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index e2a930da330..2dbb25323c2 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -152,6 +152,12 @@ export interface IChatTaskDto { kind: 'progressTask'; } +export interface IChatTaskSerialized { + content: IMarkdownString; + progress: (IChatWarningMessage | IChatContentReference)[]; + kind: 'progressTaskSerialized'; +} + export interface IChatTaskResult { content: IMarkdownString | void; kind: 'progressTaskResult'; @@ -268,6 +274,11 @@ export interface IChatExtensionsContent { kind: 'extensions'; } +export interface IChatPrepareToolInvocationPart { + readonly kind: 'prepareToolInvocation'; + readonly toolName: string; +} + export type IChatProgress = | IChatMarkdownContent | IChatAgentMarkdownContentWithVulnerability @@ -289,7 +300,9 @@ export type IChatProgress = | IChatToolInvocation | IChatToolInvocationSerialized | IChatExtensionsContent - | IChatUndoStop; + | IChatUndoStop + | IChatPrepareToolInvocationPart + | IChatTaskSerialized; export interface IChatFollowup { kind: 'reply'; @@ -525,7 +538,7 @@ export interface IChatService { onDidPerformUserAction: Event; notifyUserAction(event: IChatUserActionEvent): void; - onDidDisposeSession: Event<{ sessionId: string; reason: 'initializationFailed' | 'cleared' }>; + onDidDisposeSession: Event<{ sessionId: string; reason: 'cleared' }>; transferChatSession(transferredSessionData: IChatTransferredSessionData, toWorkspace: URI): void; diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index 1c93225df81..c5fb0f58036 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -124,7 +124,7 @@ export class ChatService extends Disposable implements IChatService { private readonly _onDidPerformUserAction = this._register(new Emitter()); public readonly onDidPerformUserAction: Event = this._onDidPerformUserAction.event; - private readonly _onDidDisposeSession = this._register(new Emitter<{ sessionId: string; reason: 'initializationFailed' | 'cleared' }>()); + private readonly _onDidDisposeSession = this._register(new Emitter<{ sessionId: string; reason: 'cleared' }>()); public readonly onDidDisposeSession = this._onDidDisposeSession.event; private readonly _sessionFollowupCancelTokens = this._register(new DisposableMap()); @@ -473,23 +473,13 @@ export class ChatService extends Disposable implements IChatService { return model; } - private async initializeSession(model: ChatModel, token: CancellationToken): Promise { - try { - this.trace('initializeSession', `Initialize session ${model.sessionId}`); - model.startInitialize(); + private initializeSession(model: ChatModel, token: CancellationToken): void { + this.trace('initializeSession', `Initialize session ${model.sessionId}`); - // Activate the default extension provided agent but do not wait - // for it to be ready so that the session can be used immediately - // without having to wait for the agent to be ready. - this.activateDefaultAgent(model.initialLocation).catch(e => this.logService.error(e)); - - model.initialize(); - } catch (err) { - this.trace('startSession', `initializeSession failed: ${err}`); - model.setInitializationError(err); - this._sessionModels.deleteAndDispose(model.sessionId); - this._onDidDisposeSession.fire({ sessionId: model.sessionId, reason: 'initializationFailed' }); - } + // Activate the default extension provided agent but do not wait + // for it to be ready so that the session can be used immediately + // without having to wait for the agent to be ready. + this.activateDefaultAgent(model.initialLocation).catch(e => this.logService.error(e)); } async activateDefaultAgent(location: ChatAgentLocation): Promise { @@ -565,8 +555,6 @@ export class ChatService extends Disposable implements IChatService { throw new Error(`Unknown session: ${request.session.sessionId}`); } - await model.waitForInitialization(); - const cts = this._pendingRequests.get(request.session.sessionId); if (cts) { this.trace('resendRequest', `Session ${request.session.sessionId} already has a pending request, cancelling...`); @@ -602,8 +590,6 @@ export class ChatService extends Disposable implements IChatService { throw new Error(`Unknown session: ${sessionId}`); } - await model.waitForInitialization(); - if (this._pendingRequests.has(sessionId)) { this.trace('sendRequest', `Session ${sessionId} already has a pending request`); return; @@ -1022,8 +1008,6 @@ export class ChatService extends Disposable implements IChatService { throw new Error(`Unknown session: ${sessionId}`); } - await model.waitForInitialization(); - const pendingRequest = this._pendingRequests.get(sessionId); if (pendingRequest?.requestId === requestId) { pendingRequest.cancel(); @@ -1042,8 +1026,6 @@ export class ChatService extends Disposable implements IChatService { throw new Error(`Unknown session: ${sessionId}`); } - await target.waitForInitialization(); - const oldOwner = request.session; target.adoptRequest(request); @@ -1064,7 +1046,6 @@ export class ChatService extends Disposable implements IChatService { throw new Error(`Unknown session: ${sessionId}`); } - await model.waitForInitialization(); const parsedRequest = typeof message === 'string' ? this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(sessionId, message) : message; diff --git a/src/vs/workbench/contrib/chat/common/chatViewModel.ts b/src/vs/workbench/contrib/chat/common/chatViewModel.ts index cab2fd082c2..6bd0aa4117e 100644 --- a/src/vs/workbench/contrib/chat/common/chatViewModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatViewModel.ts @@ -15,7 +15,7 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { ILogService } from '../../../../platform/log/common/log.js'; import { annotateVulnerabilitiesInText } from './annotations.js'; import { getFullyQualifiedId, IChatAgentCommand, IChatAgentData, IChatAgentNameService, IChatAgentResult } from './chatAgents.js'; -import { ChatModelInitState, ChatPauseState, IChatModel, IChatProgressRenderableResponseContent, IChatRequestDisablement, IChatRequestModel, IChatRequestVariableEntry, IChatResponseModel, IChatTextEditGroup, IResponse } from './chatModel.js'; +import { ChatPauseState, IChatModel, IChatProgressRenderableResponseContent, IChatRequestDisablement, IChatRequestModel, IChatRequestVariableEntry, IChatResponseModel, IChatTextEditGroup, IResponse } from './chatModel.js'; import { IParsedChatRequest } from './chatParserTypes.js'; import { ChatAgentVoteDirection, ChatAgentVoteDownReason, IChatCodeCitation, IChatContentReference, IChatFollowup, IChatProgressMessage, IChatResponseErrorDetails, IChatTask, IChatUsedContext } from './chatService.js'; import { countWords } from './chatWordCounter.js'; @@ -49,7 +49,6 @@ export interface IChatSetHiddenEvent { export interface IChatViewModel { readonly model: IChatModel; - readonly initState: ChatModelInitState; readonly sessionId: string; readonly onDidDisposeModel: Event; readonly onDidChange: Event; @@ -134,6 +133,9 @@ export interface IChatReferences { kind: 'references'; } +/** + * Content type for the "Working" progress message + */ export interface IChatWorkingProgress { kind: 'working'; isPaused: boolean; @@ -149,7 +151,7 @@ export interface IChatCodeCitations { } /** - * Type for content parts rendered by IChatListRenderer + * Type for content parts rendered by IChatListRenderer (not necessarily in the model) */ export type IChatRendererContent = IChatProgressRenderableResponseContent | IChatReferences | IChatCodeCitations | IChatWorkingProgress; @@ -240,10 +242,6 @@ export class ChatViewModel extends Disposable implements IChatViewModel { return this._model.requestPausibility; } - get initState() { - return this._model.initState; - } - constructor( private readonly _model: IChatModel, public readonly codeBlockModelCollection: CodeBlockModelCollection, @@ -344,7 +342,7 @@ export class ChatRequestViewModel implements IChatRequestViewModel { } get dataId() { - return this.id + `_${ChatModelInitState[this._model.session.initState]}_${hash(this.variables)}_${hash(this.isComplete)}`; + return this.id + `_${hash(this.variables)}_${hash(this.isComplete)}`; } get sessionId() { @@ -427,7 +425,6 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi get dataId() { return this._model.id + `_${this._modelChangeCount}` + - `_${ChatModelInitState[this._model.session.initState]}` + (this.isLast ? '_last' : ''); } diff --git a/src/vs/workbench/contrib/chat/common/constants.ts b/src/vs/workbench/contrib/chat/common/constants.ts index 020e049eaf7..33201e8807a 100644 --- a/src/vs/workbench/contrib/chat/common/constants.ts +++ b/src/vs/workbench/contrib/chat/common/constants.ts @@ -39,6 +39,10 @@ export function validateChatMode(mode: unknown): ChatMode | undefined { } } +export function isChatMode(mode: unknown): mode is ChatMode { + return !!validateChatMode(mode); +} + export type RawChatParticipantLocation = 'panel' | 'terminal' | 'notebook' | 'editing-session'; export enum ChatAgentLocation { diff --git a/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts index 15c4f7d79b1..7e38ccf197d 100644 --- a/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts @@ -38,7 +38,6 @@ export interface IToolData { */ runsInWorkspace?: boolean; alwaysDisplayInputOutput?: boolean; - supportsToolPicker?: boolean; } export interface IToolProgressStep { diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/chatPromptCodec.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/chatPromptCodec.ts index 7a48fc44494..da27950bf92 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/chatPromptCodec.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/chatPromptCodec.ts @@ -6,7 +6,28 @@ import { VSBuffer } from '../../../../../../base/common/buffer.js'; import { ReadableStream } from '../../../../../../base/common/stream.js'; import { ChatPromptDecoder, TChatPromptToken } from './chatPromptDecoder.js'; -import { ICodec } from '../../../../../../base/common/codecs/types/ICodec.js'; + +/** + * A codec is an object capable of encoding/decoding a stream of data transforming its messages. + * Useful for abstracting a data transfer or protocol logic on top of a stream of bytes. + * + * For instance, if protocol messages need to be transferred over `TCP` connection, a codec that + * encodes the messages into a sequence of bytes before sending it to a network socket. Likewise, + * on the other end of the connection, the same codec can decode the sequence of bytes back into + * a sequence of the protocol messages. + */ +export interface ICodec { + /** + * Encode a stream of `K`s into a stream of `T`s. + */ + encode: (value: ReadableStream) => ReadableStream; + + /** + * Decode a stream of `T`s into a stream of `K`s. + */ + decode: (value: ReadableStream) => ReadableStream; +} + /** * `ChatPromptCodec` type is a `ICodec` with specific types for diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/filePromptContentsProvider.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/filePromptContentsProvider.ts index f284412d543..e71accd7027 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/filePromptContentsProvider.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/filePromptContentsProvider.ts @@ -123,7 +123,7 @@ export class FilePromptContentProvider extends PromptContentsProviderBase { - if (cancellationToken?.isCancellationRequested || this.disposed) { + if (cancellationToken?.isCancellationRequested || this.isDisposed) { stream.destroy(); throw new CancellationError(); } @@ -155,7 +152,7 @@ export abstract class PromptContentsProviderBase< */ public start(): this { assert( - !this.disposed, + !this.isDisposed, 'Cannot start contents provider that was already disposed.', ); diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/textModelContentsProvider.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/textModelContentsProvider.ts index 0a109a8d97d..2a43340323b 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/textModelContentsProvider.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/textModelContentsProvider.ts @@ -38,12 +38,14 @@ export class TextModelContentsProvider extends PromptContentsProviderBase, - @IInstantiationService private readonly initService: IInstantiationService, + @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(options); this._register(this.model.onWillDispose(this.dispose.bind(this))); - this._register(this.model.onDidChangeContent(this.onChangeEmitter.fire)); + this._register( + this.model.onDidChangeContent(this.onChangeEmitter.fire.bind(this.onChangeEmitter)), + ); } /** @@ -69,14 +71,14 @@ export class TextModelContentsProvider extends PromptContentsProviderBase = {}, ): IPromptContentsProvider { if (promptContentsSource instanceof TextModel) { - return this.initService.createInstance( + return this.instantiationService.createInstance( TextModelContentsProvider, promptContentsSource, options, ); } - return this.initService.createInstance( + return this.instantiationService.createInstance( FilePromptContentProvider, promptContentsSource.uri, options, diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/types.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/types.ts index 57654814085..5eaeaefac8c 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/types.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/types.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { URI } from '../../../../../../base/common/uri.js'; +import { Event } from '../../../../../../base/common/event.js'; import { ResolveError } from '../../promptFileReferenceErrors.js'; import { IDisposable } from '../../../../../../base/common/lifecycle.js'; import { VSBufferReadableStream } from '../../../../../../base/common/buffer.js'; @@ -29,24 +30,22 @@ export interface IPromptContentsProvider extends IDisposable { */ readonly sourceName: string; - /** - * Start the contents provider to produce the underlying contents. - */ - start(): this; - /** * Event that fires when the prompt contents change. The event is either a * {@linkcode VSBufferReadableStream} stream with changed contents or * an instance of the {@linkcode ResolveError} error. */ - onContentChanged( - callback: (streamOrError: VSBufferReadableStream | ResolveError) => void, - ): IDisposable; + readonly onContentChanged: Event; /** * Subscribe to `onDispose` event of the contents provider. */ - onDispose(callback: () => void): IDisposable; + readonly onDispose: Event; + + /** + * Start the contents provider to produce the underlying contents. + */ + start(): this; /** * Create a new instance of prompt contents provider. diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/decorationsProvider/promptDecorationsProvider.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/decorationsProvider/promptDecorationsProvider.ts index 4194b2bfe0a..9d1e33e56ca 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/decorationsProvider/promptDecorationsProvider.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/decorationsProvider/promptDecorationsProvider.ts @@ -50,7 +50,7 @@ export class PromptDecorator extends ProviderInstanceBase { ): this { // by the time the promise above completes, either this object // or the text model might be already has been disposed - if (this.disposed || this.model.isDisposed()) { + if (this.isDisposed || this.model.isDisposed()) { return this; } @@ -167,7 +167,7 @@ export class PromptDecorator extends ProviderInstanceBase { } public override dispose(): void { - if (this.disposed) { + if (this.isDisposed) { return; } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/promptLinkProvider.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/promptLinkProvider.ts index 8538999ff11..e4a1484ddf0 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/promptLinkProvider.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/promptLinkProvider.ts @@ -42,7 +42,7 @@ export class PromptLinkProvider extends Disposable implements LinkProvider { const parser = this.promptsService.getSyntaxParserFor(model); assert( - !parser.disposed, + parser.isDisposed === false, 'Prompt parser must not be disposed.', ); diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/promptPathAutocompletion.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/promptPathAutocompletion.ts index 2bad7e5bbf7..f49cdb8fe29 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/promptPathAutocompletion.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/promptPathAutocompletion.ts @@ -135,7 +135,7 @@ export class PromptPathAutocompletion extends Disposable implements CompletionIt const parser = this.promptsService.getSyntaxParserFor(model); assert( - !parser.disposed, + parser.isDisposed === false, 'Prompt parser must not be disposed.', ); diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/providerInstanceManagerBase.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/providerInstanceManagerBase.ts index b1a769ae3ce..8843bf27446 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/providerInstanceManagerBase.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/providerInstanceManagerBase.ts @@ -47,7 +47,7 @@ export abstract class ProviderInstanceManagerBase * The event is fired when lines or their content change. */ private readonly _onUpdate = this._register(new Emitter()); + /** + * Subscribe to the event that is fired the parser state or contents + * changes, including changes in the possible prompt child references. + */ + public readonly onUpdate = this._onUpdate.event; /** * Event that is fired when the current prompt parser is settled. @@ -118,7 +123,7 @@ export class BasePromptParser callback: (error?: Error) => void, ): IDisposable { const disposable = this._onSettled.event(callback); - const streamEnded = (this.stream?.ended && (this.stream.disposed === false)); + const streamEnded = (this.stream?.ended && (this.stream.isDisposed === false)); // if already in the error state or stream has already ended, // invoke the callback immediately but asynchronously @@ -131,14 +136,6 @@ export class BasePromptParser return disposable; } - /** - * Subscribe to the `onUpdate` event that is fired when prompt tokens are updated. - * @param callback The callback function to be called on updates. - */ - public onUpdate(callback: () => void): IDisposable { - return this._onUpdate.event(callback); - } - /** * If failed to parse prompt contents, this property has * an error object that describes the failure reason. @@ -194,7 +191,7 @@ export class BasePromptParser // by the time when the `firstParseResult` promise is resolved, // this object may have been already disposed, hence noop - if (this.disposed) { + if (this.isDisposed) { return this; } @@ -243,8 +240,6 @@ export class BasePromptParser ...options, }; - this._onUpdate.fire = this._onUpdate.fire.bind(this._onUpdate); - const seenReferences = [...this.options.seenReferences]; // to prevent infinite file recursion, we keep track of all references in @@ -331,6 +326,13 @@ export class BasePromptParser // decode the byte stream to a stream of prompt tokens this.stream = ChatPromptCodec.decode(streamOrError); + /** + * !NOTE! The order of event subscriptions below is critical here because + * the `data` event is also starts the stream, hence changing + * the order of event subscriptions can lead to race conditions. + * See {@link ReadableStreamEvents} for more info. + */ + // on error or stream end, dispose the stream and fire the update event this.stream.on('error', this.onStreamEnd.bind(this, this.stream)); this.stream.on('end', this.onStreamEnd.bind(this, this.stream)); @@ -355,21 +357,22 @@ export class BasePromptParser // try to convert a prompt variable with data token into a file reference if (token instanceof PromptVariableWithData) { try { - this.onReference(FileReference.from(token), [...seenReferences]); + this.handleLinkToken(FileReference.from(token), [...seenReferences]); } catch (error) { - // no-op + // the `FileReference.from` call might throw if the `PromptVariableWithData` token + // can not be converted into a valid `#file` reference, hence we ignore the error } } // note! the `isURL` is a simple check and needs to be improved to truly // handle only file references, ignoring broken URLs or references if (token instanceof MarkdownLink && !token.isURL) { - this.onReference(token, [...seenReferences]); + this.handleLinkToken(token, [...seenReferences]); } }); // calling `start` on a disposed stream throws, so we warn and return instead - if (this.stream.disposed) { + if (this.stream.isDisposed) { this.logService.warn( `[prompt parser][${basename(this.uri)}] cannot start stream that has been already disposed, aborting`, ); @@ -384,7 +387,7 @@ export class BasePromptParser /** * Handle a new reference token inside prompt contents. */ - private onReference( + private handleLinkToken( token: FileReference | MarkdownLink, seenReferences: string[], ): this { @@ -401,7 +404,7 @@ export class BasePromptParser this._references.push(reference); - reference.addDisposable( + reference.addDisposables( // the content provider is exclusively owned by the reference // hence dispose it when the reference is disposed reference.onDispose(contentProvider.dispose.bind(contentProvider)), @@ -428,7 +431,7 @@ export class BasePromptParser // decoders can fire the 'end' event also when they are get disposed, // but because we dispose them when a new stream is received, we can // safely ignore the event in this case - if (stream.disposed === true) { + if (stream.isDisposed === true) { return this; } @@ -561,14 +564,6 @@ export class BasePromptParser }); } - /** - * Get list of all valid child references as URIs. - */ - public get allValidReferencesUris(): readonly URI[] { - return this.allValidReferences - .map(child => child.uri); - } - /** * Valid metadata records defined in the prompt header. */ @@ -756,7 +751,7 @@ export class BasePromptParser * @inheritdoc */ public override dispose(): void { - if (this.disposed) { + if (this.isDisposed) { return; } @@ -787,11 +782,11 @@ export class PromptReference extends ObservableDisposable implements TPromptRefe private readonly promptContentsProvider: IPromptContentsProvider, public readonly token: FileReference | MarkdownLink, options: Partial, - @IInstantiationService initService: IInstantiationService, + @IInstantiationService instantiationService: IInstantiationService, ) { super(); - this.parser = this._register(initService.createInstance( + this.parser = this._register(instantiationService.createInstance( BasePromptParser, this.promptContentsProvider, options, @@ -864,10 +859,9 @@ export class PromptReference extends ObservableDisposable implements TPromptRefe /** * Subscribe to the `onUpdate` event that is fired when prompt tokens are updated. - * @param callback The callback function to be called on updates. */ - public onUpdate(callback: () => void): IDisposable { - return this.parser.onUpdate(callback); + public onUpdate(...args: Parameters>): ReturnType> { + return this.parser.onUpdate(...args); } public get range(): Range { diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/filePromptParser.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/filePromptParser.ts index b2078735461..f92c89969e2 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/filePromptParser.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/filePromptParser.ts @@ -18,12 +18,12 @@ export class FilePromptParser extends BasePromptParser, - @IInstantiationService initService: IInstantiationService, + @IInstantiationService instantiationService: IInstantiationService, @IWorkspaceContextService workspaceService: IWorkspaceContextService, @ILogService logService: ILogService, ) { - const contentsProvider = initService.createInstance(FilePromptContentProvider, uri, options); - super(contentsProvider, options, initService, workspaceService, logService); + const contentsProvider = instantiationService.createInstance(FilePromptContentProvider, uri, options); + super(contentsProvider, options, instantiationService, workspaceService, logService); this._register(contentsProvider); } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/textModelPromptParser.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/textModelPromptParser.ts index e2ebc518235..5c6905f2f28 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/textModelPromptParser.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/textModelPromptParser.ts @@ -18,17 +18,17 @@ export class TextModelPromptParser extends BasePromptParser, - @IInstantiationService initService: IInstantiationService, + @IInstantiationService instantiationService: IInstantiationService, @IWorkspaceContextService workspaceService: IWorkspaceContextService, @ILogService logService: ILogService, ) { - const contentsProvider = initService.createInstance( + const contentsProvider = instantiationService.createInstance( TextModelContentsProvider, model, options, ); - super(contentsProvider, options, initService, workspaceService, logService); + super(contentsProvider, options, instantiationService, workspaceService, logService); this._register(contentsProvider); } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts index 4c0c59e73f3..b86cbf24283 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts @@ -55,12 +55,12 @@ export class PromptsService extends Disposable implements IPromptsService { @ILogService public readonly logger: ILogService, @ILabelService private readonly labelService: ILabelService, @IModelService private readonly modelService: IModelService, - @IInstantiationService private readonly initService: IInstantiationService, + @IInstantiationService private readonly instantiationService: IInstantiationService, @IUserDataProfileService private readonly userDataService: IUserDataProfileService, ) { super(); - this.fileLocator = this.initService.createInstance(PromptFilesLocator); + this.fileLocator = this.instantiationService.createInstance(PromptFilesLocator); this.logTime = this.logger.trace.bind(this.logger); // the factory function below creates a new prompt parser object @@ -77,7 +77,7 @@ export class PromptsService extends Disposable implements IPromptsService { * Otherwise consumers will either see incorrect failing or incorrect successful results, based on their * use case, timing of their calls to the {@link getSyntaxParserFor} function, and state of this service. */ - const parser: TextModelPromptParser = initService.createInstance( + const parser: TextModelPromptParser = instantiationService.createInstance( TextModelPromptParser, model, { seenReferences: [] }, @@ -102,7 +102,7 @@ export class PromptsService extends Disposable implements IPromptsService { */ public getSyntaxParserFor( model: ITextModel, - ): TextModelPromptParser & { disposed: false } { + ): TextModelPromptParser & { isDisposed: false } { assert( model.isDisposed() === false, 'Cannot create a prompt syntax parser for a disposed model.', @@ -144,7 +144,7 @@ export class PromptsService extends Disposable implements IPromptsService { } public asPromptSlashCommand(command: string): IChatPromptSlashCommand | undefined { - if (command.match(/^[\w_\-\.]+/)) { + if (command.match(/^[\w_\-\.]+$/)) { return { command, detail: localize('prompt.file.detail', 'Prompt file: {0}', command) }; } return undefined; @@ -183,17 +183,16 @@ export class PromptsService extends Disposable implements IPromptsService { public async findInstructionFilesFor( files: readonly URI[], ): Promise { - const result: URI[] = []; - const instructionFiles = await this.listPromptFiles('instructions'); if (instructionFiles.length === 0) { - return result; + return []; } const instructions = await this.getAllMetadata( instructionFiles.map(pick('uri')), ); + const foundFiles = new ResourceSet(); for (const instruction of instructions.flatMap(flatten)) { const { metadata, uri } = instruction; const { applyTo } = metadata; @@ -205,7 +204,7 @@ export class PromptsService extends Disposable implements IPromptsService { // if glob pattern is one of the special wildcard values, // add the instructions file event if no files are attached if ((applyTo === '**') || (applyTo === '**/*')) { - result.push(uri); + foundFiles.add(uri); continue; } @@ -214,12 +213,12 @@ export class PromptsService extends Disposable implements IPromptsService { // add the instructions file if its rule matches the file for (const file of files) { if (match(applyTo, file.fsPath)) { - result.push(uri); + foundFiles.add(uri); } } } - return [...new ResourceSet(result)]; + return [...foundFiles]; } @logTime() @@ -230,7 +229,7 @@ export class PromptsService extends Disposable implements IPromptsService { promptUris.map(async (uri) => { let parser: PromptParser | undefined; try { - parser = this.initService.createInstance( + parser = this.instantiationService.createInstance( PromptParser, uri, { allowNonPromptFiles: true }, diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/types.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/types.ts index e9faf6a2f40..01ffd796310 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/types.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/types.ts @@ -128,7 +128,7 @@ export interface IPromptsService extends IDisposable { */ getSyntaxParserFor( model: ITextModel, - ): TSharedPrompt & { disposed: false }; + ): TSharedPrompt & { isDisposed: false }; /** * List all available prompt files. diff --git a/src/vs/workbench/contrib/chat/common/tools/languageModelToolsContribution.ts b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsContribution.ts index d3acab61bdc..5acc4fd9a3b 100644 --- a/src/vs/workbench/contrib/chat/common/tools/languageModelToolsContribution.ts +++ b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsContribution.ts @@ -199,7 +199,6 @@ export class LanguageModelToolsExtensionPointHandler implements IWorkbenchContri icon, when: rawTool.when ? ContextKeyExpr.deserialize(rawTool.when) : undefined, alwaysDisplayInputOutput: !isBuiltinTool, - supportsToolPicker: rawTool.canBeReferencedInPrompt }; const disposable = languageModelToolsService.registerToolData(tool); this._registrationDisposables.set(toToolKey(extension.description.identifier, rawTool.name), disposable); diff --git a/src/vs/workbench/contrib/chat/electron-sandbox/actions/chatDeveloperActions.ts b/src/vs/workbench/contrib/chat/electron-sandbox/actions/chatDeveloperActions.ts index 55db9683c2e..d9f8fb12271 100644 --- a/src/vs/workbench/contrib/chat/electron-sandbox/actions/chatDeveloperActions.ts +++ b/src/vs/workbench/contrib/chat/electron-sandbox/actions/chatDeveloperActions.ts @@ -8,6 +8,7 @@ import { localize2 } from '../../../../../nls.js'; import { Categories } from '../../../../../platform/action/common/actionCommonCategories.js'; import { Action2, registerAction2 } from '../../../../../platform/actions/common/actions.js'; import { INativeHostService } from '../../../../../platform/native/common/native.js'; +import { ChatContextKeys } from '../../common/chatContextKeys.js'; import { IChatService } from '../../common/chatService.js'; export function registerChatDeveloperActions() { @@ -23,7 +24,8 @@ class OpenChatStorageFolderAction extends Action2 { title: localize2('workbench.action.chat.openStorageFolder.label', "Open Chat Storage Folder"), icon: Codicon.attach, category: Categories.Developer, - f1: true + f1: true, + precondition: ChatContextKeys.enabled }); } diff --git a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts b/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts index 825f843715f..6dfcdf8e3ec 100644 --- a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts +++ b/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts @@ -543,10 +543,10 @@ export class QuickVoiceChatAction extends VoiceChatWithHoldModeAction { const primaryVoiceActionMenu = (when: ContextKeyExpression | undefined) => { return [ { - id: MenuId.ChatInput, + id: MenuId.ChatExecute, when: ContextKeyExpr.and(ChatContextKeys.location.isEqualTo(ChatAgentLocation.Panel), when), group: 'navigation', - order: 0 + order: 3 }, { id: MenuId.ChatExecute, diff --git a/src/vs/workbench/contrib/chat/electron-sandbox/tools/fetchPageTool.ts b/src/vs/workbench/contrib/chat/electron-sandbox/tools/fetchPageTool.ts index 632a9b9bbc3..c037d3c729c 100644 --- a/src/vs/workbench/contrib/chat/electron-sandbox/tools/fetchPageTool.ts +++ b/src/vs/workbench/contrib/chat/electron-sandbox/tools/fetchPageTool.ts @@ -5,10 +5,10 @@ import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { MarkdownString } from '../../../../../base/common/htmlContent.js'; +import { ResourceSet } from '../../../../../base/common/map.js'; import { URI } from '../../../../../base/common/uri.js'; import { localize } from '../../../../../nls.js'; import { IWebContentExtractorService } from '../../../../../platform/webContentExtractor/common/webContentExtractor.js'; -import { ITrustedDomainService } from '../../../url/browser/trustedDomainService.js'; import { CountTokensCallback, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolResult, IToolResultTextPart, ToolProgress } from '../../common/languageModelToolsService.js'; import { InternalFetchWebPageToolId } from '../../common/tools/tools.js'; @@ -34,11 +34,10 @@ export const FetchWebPageToolData: IToolData = { }; export class FetchWebPageTool implements IToolImpl { - private _alreadyApprovedDomains = new Set(); + private _alreadyApprovedDomains = new ResourceSet(); constructor( @IWebContentExtractorService private readonly _readerModeService: IWebContentExtractorService, - @ITrustedDomainService private readonly _trustedDomainService: ITrustedDomainService, ) { } async invoke(invocation: IToolInvocation, _countTokens: CountTokensCallback, _progress: ToolProgress, _token: CancellationToken): Promise { @@ -53,9 +52,7 @@ export class FetchWebPageTool implements IToolImpl { // We approved these via confirmation, so mark them as "approved" in this session // if they are not approved via the trusted domain service. for (const uri of validUris) { - if (!this._trustedDomainService.isValid(uri)) { - this._alreadyApprovedDomains.add(uri.toString(true)); - } + this._alreadyApprovedDomains.add(uri); } const contents = await this._readerModeService.extract(validUris); @@ -89,7 +86,7 @@ export class FetchWebPageTool implements IToolImpl { valid.push(uri); } }); - const urlsNeedingConfirmation = valid.filter(url => !this._trustedDomainService.isValid(url) && !this._alreadyApprovedDomains.has(url.toString(true))); + const urlsNeedingConfirmation = valid.filter(url => !this._alreadyApprovedDomains.has(url)); const pastTenseMessage = invalid.length ? invalid.length > 1 @@ -138,32 +135,17 @@ export class FetchWebPageTool implements IToolImpl { const result: IPreparedToolInvocation = { invocationMessage, pastTenseMessage }; if (urlsNeedingConfirmation.length) { - const confirmationTitle = urlsNeedingConfirmation.length > 1 - ? localize('fetchWebPage.confirmationTitle.plural', 'Fetch untrusted web pages?') - : localize('fetchWebPage.confirmationTitle.singular', 'Fetch untrusted web page?'); - - const managedTrustedDomainsCommand = 'workbench.action.manageTrustedDomain'; - const confirmationMessage = new MarkdownString( - urlsNeedingConfirmation.length > 1 - ? urlsNeedingConfirmation.map(uri => `- ${uri.toString()}`).join('\n') - : urlsNeedingConfirmation[0].toString(), - { - isTrusted: { enabledCommands: [managedTrustedDomainsCommand] }, - supportThemeIcons: true - } - ); - - confirmationMessage.appendMarkdown( - '\n\n$(info) ' + localize( - 'fetchWebPage.confirmationMessageManageTrustedDomains', - 'You can [manage your trusted domains]({0}) to skip this confirmation in the future.', - `command:${managedTrustedDomainsCommand}` - ) - ); - - result.confirmationMessages = { title: confirmationTitle, message: confirmationMessage, allowAutoConfirm: false }; + let confirmationTitle: string; + let confirmationMessage: string | MarkdownString; + if (urlsNeedingConfirmation.length === 1) { + confirmationTitle = localize('fetchWebPage.confirmationTitle.singular', 'Fetch untrusted web page?'); + confirmationMessage = urlsNeedingConfirmation[0].toString(); + } else { + confirmationTitle = localize('fetchWebPage.confirmationTitle.plural', 'Fetch untrusted web pages?'); + confirmationMessage = new MarkdownString(urlsNeedingConfirmation.map(uri => `- ${uri.toString()}`).join('\n')); + } + result.confirmationMessages = { title: confirmationTitle, message: confirmationMessage, allowAutoConfirm: true }; } - return result; } diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_remote_images.0.snap b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_remote_images.0.snap deleted file mode 100644 index 1241ef62b5f..00000000000 --- a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_remote_images.0.snap +++ /dev/null @@ -1 +0,0 @@ -

<img src="http://disallowed.com/image.jpg">

\ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_remote_images_are_disallowed.0.snap b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_remote_images_are_disallowed.0.snap new file mode 100644 index 00000000000..99f7cb267ec --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_remote_images_are_disallowed.0.snap @@ -0,0 +1 @@ +

<img src="http://disallowed.com/image.jpg">

\ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/browser/chatEditingSessionStorage.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatEditingSessionStorage.test.ts index bbafcaad2ac..cd07b1d1c9d 100644 --- a/src/vs/workbench/contrib/chat/test/browser/chatEditingSessionStorage.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/chatEditingSessionStorage.test.ts @@ -9,8 +9,8 @@ import { cloneAndChange } from '../../../../../base/common/objects.js'; import { URI } from '../../../../../base/common/uri.js'; import { generateUuid } from '../../../../../base/common/uuid.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; -import { OffsetEdit } from '../../../../../editor/common/core/edits/offsetEdit.js'; -import { OffsetRange } from '../../../../../editor/common/core/offsetRange.js'; +import { StringEdit } from '../../../../../editor/common/core/edits/stringEdit.js'; +import { OffsetRange } from '../../../../../editor/common/core/ranges/offsetRange.js'; import { FileService } from '../../../../../platform/files/common/fileService.js'; import { InMemoryFileSystemProvider } from '../../../../../platform/files/common/inMemoryFilesystemProvider.js'; import { NullLogService } from '../../../../../platform/log/common/log.js'; @@ -51,7 +51,7 @@ suite('ChatEditingSessionStorage', () => { return { stopId, entries: new ResourceMap([ - [resource, { resource, languageId: 'javascript', snapshotUri: ChatEditingSnapshotTextModelContentProvider.getSnapshotFileURI(sessionId, requestId, stopId, resource.path), original: `contents${before}}`, current: `contents${after}`, originalToCurrentEdit: OffsetEdit.replace(OffsetRange.ofLength(42), 'newtext'), state: ModifiedFileEntryState.Modified, telemetryInfo: { agentId: 'agentId', command: 'cmd', requestId: generateUuid(), result: undefined, sessionId } } satisfies ISnapshotEntry], + [resource, { resource, languageId: 'javascript', snapshotUri: ChatEditingSnapshotTextModelContentProvider.getSnapshotFileURI(sessionId, requestId, stopId, resource.path), original: `contents${before}}`, current: `contents${after}`, originalToCurrentEdit: StringEdit.replace(OffsetRange.ofLength(42), 'newtext'), state: ModifiedFileEntryState.Modified, telemetryInfo: { agentId: 'agentId', command: 'cmd', requestId: generateUuid(), result: undefined, sessionId } } satisfies ISnapshotEntry], ]), }; } diff --git a/src/vs/workbench/contrib/chat/test/browser/chatMarkdownRenderer.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatMarkdownRenderer.test.ts index 11e622395c4..8b12f3194a7 100644 --- a/src/vs/workbench/contrib/chat/test/browser/chatMarkdownRenderer.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/chatMarkdownRenderer.test.ts @@ -7,8 +7,6 @@ import { MarkdownString } from '../../../../../base/common/htmlContent.js'; import { assertSnapshot } from '../../../../../base/test/common/snapshot.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; import { ChatMarkdownRenderer } from '../../browser/chatMarkdownRenderer.js'; -import { ITrustedDomainService } from '../../../url/browser/trustedDomainService.js'; -import { MockTrustedDomainService } from '../../../url/test/browser/mockTrustedDomainService.js'; import { workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js'; suite('ChatMarkdownRenderer', () => { @@ -17,7 +15,6 @@ suite('ChatMarkdownRenderer', () => { let testRenderer: ChatMarkdownRenderer; setup(() => { const instantiationService = store.add(workbenchInstantiationService(undefined, store)); - instantiationService.stub(ITrustedDomainService, new MockTrustedDomainService(['http://allowed.com'])); testRenderer = instantiationService.createInstance(ChatMarkdownRenderer, {}); }); @@ -102,8 +99,8 @@ suite('ChatMarkdownRenderer', () => { await assertSnapshot(result.element.outerHTML); }); - test('remote images', async () => { - const md = new MarkdownString(' '); + test('remote images are disallowed', async () => { + const md = new MarkdownString(''); md.supportHtml = true; const result = store.add(testRenderer.render(md)); await assertSnapshot(result.element.outerHTML); diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_prompt_slash_command.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_prompt_slash_command.0.snap new file mode 100644 index 00000000000..70b24f7309e --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_prompt_slash_command.0.snap @@ -0,0 +1,33 @@ +{ + parts: [ + { + range: { + start: 0, + endExclusive: 4 + }, + editorRange: { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 5 + }, + text: " ", + kind: "text" + }, + { + range: { + start: 4, + endExclusive: 11 + }, + editorRange: { + startLineNumber: 1, + startColumn: 5, + endLineNumber: 1, + endColumn: 12 + }, + slashPromptCommand: { command: "prompt" }, + kind: "prompt" + } + ], + text: " /prompt" +} \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_prompt_slash_command_after_slash.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_prompt_slash_command_after_slash.0.snap new file mode 100644 index 00000000000..ce48a80a13a --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_prompt_slash_command_after_slash.0.snap @@ -0,0 +1,19 @@ +{ + parts: [ + { + range: { + start: 0, + endExclusive: 41 + }, + editorRange: { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 42 + }, + text: "/ route and the request of /search-option", + kind: "text" + } + ], + text: "/ route and the request of /search-option" +} \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_prompt_slash_command_after_text.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_prompt_slash_command_after_text.0.snap new file mode 100644 index 00000000000..120c1cc46dd --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_prompt_slash_command_after_text.0.snap @@ -0,0 +1,19 @@ +{ + parts: [ + { + range: { + start: 0, + endExclusive: 52 + }, + editorRange: { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 53 + }, + text: "handle the / route and the request of /search-option", + kind: "text" + } + ], + text: "handle the / route and the request of /search-option" +} \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_slash_command_after_whitespace.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_slash_command_after_whitespace.0.snap new file mode 100644 index 00000000000..b0f2a2c0d40 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_slash_command_after_whitespace.0.snap @@ -0,0 +1,33 @@ +{ + parts: [ + { + range: { + start: 0, + endExclusive: 4 + }, + editorRange: { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 5 + }, + text: " ", + kind: "text" + }, + { + range: { + start: 4, + endExclusive: 8 + }, + editorRange: { + startLineNumber: 1, + startColumn: 5, + endLineNumber: 1, + endColumn: 9 + }, + slashCommand: { command: "fix" }, + kind: "slash" + } + ], + text: " /fix" +} \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_slash_command_not_first.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_slash_command_not_first.0.snap new file mode 100644 index 00000000000..b36f6191154 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_slash_command_not_first.0.snap @@ -0,0 +1,19 @@ +{ + parts: [ + { + range: { + start: 0, + endExclusive: 10 + }, + editorRange: { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 11 + }, + text: "Hello /fix", + kind: "text" + } + ], + text: "Hello /fix" +} \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_slash_in_text.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_slash_in_text.0.snap new file mode 100644 index 00000000000..4a0dd5fb12d --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_slash_in_text.0.snap @@ -0,0 +1,19 @@ +{ + parts: [ + { + range: { + start: 0, + endExclusive: 65 + }, + editorRange: { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 66 + }, + text: "can we add a new file for an Express router to handle the / route", + kind: "text" + } + ], + text: "can we add a new file for an Express router to handle the / route" +} \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts b/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts index 768f7892ded..4ccbeaaa588 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts @@ -4,26 +4,25 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; -import { timeout } from '../../../../../base/common/async.js'; import { MarkdownString } from '../../../../../base/common/htmlContent.js'; import { URI } from '../../../../../base/common/uri.js'; import { assertSnapshot } from '../../../../../base/test/common/snapshot.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; -import { OffsetRange } from '../../../../../editor/common/core/offsetRange.js'; import { Range } from '../../../../../editor/common/core/range.js'; +import { OffsetRange } from '../../../../../editor/common/core/ranges/offsetRange.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; import { MockContextKeyService } from '../../../../../platform/keybinding/test/common/mockKeybindingService.js'; import { ILogService, NullLogService } from '../../../../../platform/log/common/log.js'; import { IStorageService } from '../../../../../platform/storage/common/storage.js'; +import { IExtensionService } from '../../../../services/extensions/common/extensions.js'; +import { TestExtensionService, TestStorageService } from '../../../../test/common/workbenchTestServices.js'; import { ChatAgentService, IChatAgentService } from '../../common/chatAgents.js'; import { ChatModel, ISerializableChatData1, ISerializableChatData2, ISerializableChatData3, normalizeSerializableChatData, Response } from '../../common/chatModel.js'; import { ChatRequestTextPart } from '../../common/chatParserTypes.js'; -import { IExtensionService } from '../../../../services/extensions/common/extensions.js'; -import { TestExtensionService, TestStorageService } from '../../../../test/common/workbenchTestServices.js'; import { ChatAgentLocation } from '../../common/constants.js'; -import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; -import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; suite('ChatModel', () => { const testDisposables = ensureNoDisposablesAreLeakedInTestSuite(); @@ -40,83 +39,9 @@ suite('ChatModel', () => { instantiationService.stub(IConfigurationService, new TestConfigurationService()); }); - test('Waits for initialization', async () => { - const model = testDisposables.add(instantiationService.createInstance(ChatModel, undefined, ChatAgentLocation.Panel)); - - let hasInitialized = false; - model.waitForInitialization().then(() => { - hasInitialized = true; - }); - - await timeout(0); - assert.strictEqual(hasInitialized, false); - - model.startInitialize(); - model.initialize(undefined); - await timeout(0); - assert.strictEqual(hasInitialized, true); - }); - - test('must call startInitialize before initialize', async () => { - const model = testDisposables.add(instantiationService.createInstance(ChatModel, undefined, ChatAgentLocation.Panel)); - - let hasInitialized = false; - model.waitForInitialization().then(() => { - hasInitialized = true; - }); - - await timeout(0); - assert.strictEqual(hasInitialized, false); - - assert.throws(() => model.initialize(undefined)); - assert.strictEqual(hasInitialized, false); - }); - - test('deinitialize/reinitialize', async () => { - const model = testDisposables.add(instantiationService.createInstance(ChatModel, undefined, ChatAgentLocation.Panel)); - - let hasInitialized = false; - model.waitForInitialization().then(() => { - hasInitialized = true; - }); - - model.startInitialize(); - model.initialize(undefined); - await timeout(0); - assert.strictEqual(hasInitialized, true); - - model.deinitialize(); - let hasInitialized2 = false; - model.waitForInitialization().then(() => { - hasInitialized2 = true; - }); - - model.startInitialize(); - model.initialize(undefined); - await timeout(0); - assert.strictEqual(hasInitialized2, true); - }); - - test('cannot initialize twice', async () => { - const model = testDisposables.add(instantiationService.createInstance(ChatModel, undefined, ChatAgentLocation.Panel)); - - model.startInitialize(); - model.initialize(undefined); - assert.throws(() => model.initialize(undefined)); - }); - - test('Initialization fails when model is disposed', async () => { - const model = testDisposables.add(instantiationService.createInstance(ChatModel, undefined, ChatAgentLocation.Panel)); - model.dispose(); - - assert.throws(() => model.initialize(undefined)); - }); - test('removeRequest', async () => { const model = testDisposables.add(instantiationService.createInstance(ChatModel, undefined, ChatAgentLocation.Panel)); - model.startInitialize(); - model.initialize(undefined); const text = 'hello'; model.addRequest({ text, parts: [new ChatRequestTextPart(new OffsetRange(0, text.length), new Range(1, text.length, 1, text.length), text)] }, { variables: [] }, 0); const requests = model.getRequests(); @@ -130,12 +55,6 @@ suite('ChatModel', () => { const model1 = testDisposables.add(instantiationService.createInstance(ChatModel, undefined, ChatAgentLocation.Editor)); const model2 = testDisposables.add(instantiationService.createInstance(ChatModel, undefined, ChatAgentLocation.Panel)); - model1.startInitialize(); - model1.initialize(undefined); - - model2.startInitialize(); - model2.initialize(undefined); - const text = 'hello'; const request1 = model1.addRequest({ text, parts: [new ChatRequestTextPart(new OffsetRange(0, text.length), new Range(1, text.length, 1, text.length), text)] }, { variables: [] }, 0); @@ -159,9 +78,6 @@ suite('ChatModel', () => { test('addCompleteRequest', async function () { const model1 = testDisposables.add(instantiationService.createInstance(ChatModel, undefined, ChatAgentLocation.Panel)); - model1.startInitialize(); - model1.initialize(undefined); - const text = 'hello'; const request1 = model1.addRequest({ text, parts: [new ChatRequestTextPart(new OffsetRange(0, text.length), new Range(1, text.length, 1, text.length), text)] }, { variables: [] }, 0, undefined, undefined, undefined, undefined, undefined, true); diff --git a/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts b/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts index 6d8091a23fa..a3b527ade6c 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts @@ -61,6 +61,13 @@ suite('ChatRequestParser', () => { await assertSnapshot(result); }); + test('slash in text', async () => { + parser = instantiationService.createInstance(ChatRequestParser); + const text = 'can we add a new file for an Express router to handle the / route'; + const result = parser.parseChatRequest('1', text); + await assertSnapshot(result); + }); + test('slash command', async () => { const slashCommandService = mockObject()({}); slashCommandService.getCommands.returns([{ command: 'fix' }]); @@ -94,6 +101,89 @@ suite('ChatRequestParser', () => { await assertSnapshot(result); }); + test('slash command not first', async () => { + const slashCommandService = mockObject()({}); + slashCommandService.getCommands.returns([{ command: 'fix' }]); + instantiationService.stub(IChatSlashCommandService, slashCommandService as any); + + parser = instantiationService.createInstance(ChatRequestParser); + const text = 'Hello /fix'; + const result = parser.parseChatRequest('1', text); + await assertSnapshot(result); + }); + + test('slash command after whitespace', async () => { + const slashCommandService = mockObject()({}); + slashCommandService.getCommands.returns([{ command: 'fix' }]); + instantiationService.stub(IChatSlashCommandService, slashCommandService as any); + + parser = instantiationService.createInstance(ChatRequestParser); + const text = ' /fix'; + const result = parser.parseChatRequest('1', text); + await assertSnapshot(result); + }); + + test('prompt slash command', async () => { + const slashCommandService = mockObject()({}); + slashCommandService.getCommands.returns([{ command: 'fix' }]); + instantiationService.stub(IChatSlashCommandService, slashCommandService as any); + + const promptSlashCommandService = mockObject()({}); + promptSlashCommandService.asPromptSlashCommand.callsFake((command: string) => { + if (command.match(/^[\w_\-\.]+$/)) { + return { command }; + } + return undefined; + }); + instantiationService.stub(IPromptsService, promptSlashCommandService as any); + + parser = instantiationService.createInstance(ChatRequestParser); + const text = ' /prompt'; + const result = parser.parseChatRequest('1', text); + await assertSnapshot(result); + }); + + test('prompt slash command after text', async () => { + const slashCommandService = mockObject()({}); + slashCommandService.getCommands.returns([{ command: 'fix' }]); + instantiationService.stub(IChatSlashCommandService, slashCommandService as any); + + const promptSlashCommandService = mockObject()({}); + promptSlashCommandService.asPromptSlashCommand.callsFake((command: string) => { + if (command.match(/^[\w_\-\.]+$/)) { + return { command }; + } + return undefined; + }); + instantiationService.stub(IPromptsService, promptSlashCommandService as any); + + parser = instantiationService.createInstance(ChatRequestParser); + const text = 'handle the / route and the request of /search-option'; + const result = parser.parseChatRequest('1', text); + await assertSnapshot(result); + }); + + test('prompt slash command after slash', async () => { + const slashCommandService = mockObject()({}); + slashCommandService.getCommands.returns([{ command: 'fix' }]); + instantiationService.stub(IChatSlashCommandService, slashCommandService as any); + + const promptSlashCommandService = mockObject()({}); + promptSlashCommandService.asPromptSlashCommand.callsFake((command: string) => { + if (command.match(/^[\w_\-\.]+$/)) { + return { command }; + } + return undefined; + }); + instantiationService.stub(IPromptsService, promptSlashCommandService as any); + + parser = instantiationService.createInstance(ChatRequestParser); + const text = '/ route and the request of /search-option'; + const result = parser.parseChatRequest('1', text); + await assertSnapshot(result); + }); + + // test('variables', async () => { // varService.hasVariable.returns(true); // varService.getVariable.returns({ id: 'copilot.selection' }); diff --git a/src/vs/workbench/contrib/chat/test/common/chatService.test.ts b/src/vs/workbench/contrib/chat/test/common/chatService.test.ts index 0d06d63cdfb..8f5c8e3bc06 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatService.test.ts @@ -162,19 +162,15 @@ suite('ChatService', () => { test('retrieveSession', async () => { const testService = testDisposables.add(instantiationService.createInstance(ChatService)); const session1 = testDisposables.add(testService.startSession(ChatAgentLocation.Panel, CancellationToken.None)); - await session1.waitForInitialization(); session1.addRequest({ parts: [], text: 'request 1' }, { variables: [] }, 0); const session2 = testDisposables.add(testService.startSession(ChatAgentLocation.Panel, CancellationToken.None)); - await session2.waitForInitialization(); session2.addRequest({ parts: [], text: 'request 2' }, { variables: [] }, 0); storageService.flush(); const testService2 = testDisposables.add(instantiationService.createInstance(ChatService)); const retrieved1 = testDisposables.add((await testService2.getOrRestoreSession(session1.sessionId))!); - await retrieved1.waitForInitialization(); const retrieved2 = testDisposables.add((await testService2.getOrRestoreSession(session2.sessionId))!); - await retrieved2.waitForInitialization(); assert.deepStrictEqual(retrieved1.getRequests()[0]?.message.text, 'request 1'); assert.deepStrictEqual(retrieved2.getRequests()[0]?.message.text, 'request 2'); }); diff --git a/src/vs/workbench/contrib/chat/test/common/mockChatService.ts b/src/vs/workbench/contrib/chat/test/common/mockChatService.ts index 346e678252d..b3dbb032861 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockChatService.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockChatService.ts @@ -82,7 +82,7 @@ export class MockChatService implements IChatService { notifyUserAction(event: IChatUserActionEvent): void { throw new Error('Method not implemented.'); } - onDidDisposeSession: Event<{ sessionId: string; reason: 'initializationFailed' | 'cleared' }> = undefined!; + onDidDisposeSession: Event<{ sessionId: string; reason: 'cleared' }> = undefined!; transferChatSession(transferredSessionData: IChatTransferredSessionData, toWorkspace: URI): void { throw new Error('Method not implemented.'); diff --git a/src/vs/workbench/contrib/chat/test/common/mockPromptsService.ts b/src/vs/workbench/contrib/chat/test/common/mockPromptsService.ts index 0683ce71f9e..9e9fb46b4ac 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockPromptsService.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockPromptsService.ts @@ -5,43 +5,37 @@ import { URI } from '../../../../../base/common/uri.js'; import { ITextModel } from '../../../../../editor/common/model.js'; -import { PROMPT_FILE_EXTENSION } from '../../../../../platform/prompts/common/constants.js'; import { TextModelPromptParser } from '../../common/promptSyntax/parsers/textModelPromptParser.js'; import { IChatPromptSlashCommand, IMetadata, IPromptPath, IPromptsService, TCombinedToolsMetadata, TPromptsType } from '../../common/promptSyntax/service/types.js'; export class MockPromptsService implements IPromptsService { + _serviceBrand: undefined; + getCombinedToolsMetadata(files: readonly URI[]): Promise { throw new Error('Method not implemented.'); } - getAllMetadata(files: readonly URI[]): Promise { + getAllMetadata(_files: readonly URI[]): Promise { throw new Error('Method not implemented.'); } - _serviceBrand: undefined; - getSyntaxParserFor(model: ITextModel): TextModelPromptParser & { disposed: false } { + getSyntaxParserFor(_model: ITextModel): TextModelPromptParser & { isDisposed: false } { throw new Error('Method not implemented.'); } - listPromptFiles(type: TPromptsType): Promise { + listPromptFiles(_type: TPromptsType): Promise { throw new Error('Method not implemented.'); } - getSourceFolders(type: TPromptsType): readonly IPromptPath[] { + getSourceFolders(_type: TPromptsType): readonly IPromptPath[] { throw new Error('Method not implemented.'); } - public asPromptSlashCommand(name: string): IChatPromptSlashCommand | undefined { - if (name.endsWith(PROMPT_FILE_EXTENSION)) { - const command = `prompt:${name.substring(0, -PROMPT_FILE_EXTENSION.length)}`; - return { - command, detail: name, - }; - } + public asPromptSlashCommand(command: string): IChatPromptSlashCommand | undefined { return undefined; } - resolvePromptSlashCommand(data: IChatPromptSlashCommand): Promise { + resolvePromptSlashCommand(_data: IChatPromptSlashCommand): Promise { throw new Error('Method not implemented.'); } findPromptSlashCommands(): Promise { throw new Error('Method not implemented.'); } - findInstructionFilesFor(files: readonly URI[]): Promise { + findInstructionFilesFor(_files: readonly URI[]): Promise { throw new Error('Method not implemented.'); } dispose(): void { } diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/parsers/textModelPromptParser.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/parsers/textModelPromptParser.test.ts index bcbd2f29f5f..f72e94e3104 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/parsers/textModelPromptParser.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/parsers/textModelPromptParser.test.ts @@ -46,7 +46,7 @@ class TextModelPromptParserTest extends Disposable { initialContents: string[], languageId: string = PROMPT_LANGUAGE_ID, @IFileService fileService: IFileService, - @IInstantiationService initService: IInstantiationService, + @IInstantiationService instantiationService: IInstantiationService, ) { super(); @@ -69,7 +69,7 @@ class TextModelPromptParserTest extends Disposable { // create the parser instance this.parser = this._register( - initService.createInstance(TextModelPromptParser, this.model, {}), + instantiationService.createInstance(TextModelPromptParser, this.model, {}), ).start(); } @@ -290,6 +290,51 @@ suite('TextModelPromptParser', () => { suite('• header', () => { suite(' • metadata', () => { + test(`• empty header`, async () => { + const test = createTest( + URI.file('/absolute/folder/and/a/filename.txt'), + [ + /* 01 */"---", + /* 02 */"", + /* 03 */"---", + /* 04 */"The cactus on my desk has a thriving Instagram account.", + /* 05 */"Midnight snacks are the secret to eternal [text](./foo-bar-baz/another-file.ts) happiness.", + /* 06 */"In an alternate universe, pigeons deliver sushi by drone.", + /* 07 */"Lunar rainbows only appear when you sing in falsetto.", + /* 08 */"Carrots have secret telepathic abilities, but only on Tuesdays.", + ], + ); + + await test.validateReferences([ + new ExpectedReference({ + uri: URI.file('/absolute/folder/and/a/foo-bar-baz/another-file.ts'), + text: '[text](./foo-bar-baz/another-file.ts)', + path: './foo-bar-baz/another-file.ts', + startLine: 5, + startColumn: 43, + pathStartColumn: 50, + childrenOrError: new OpenFailed(URI.file('/absolute/folder/and/a/foo-bar-baz/another-file.ts'), 'File not found.'), + }), + ]); + + const { header, metadata } = test.parser; + assertDefined( + header, + 'Prompt header must be defined.', + ); + + assert.deepStrictEqual( + metadata, + { + applyTo: undefined, + description: undefined, + mode: undefined, + tools: undefined, + }, + 'Must have empty metadata.', + ); + }); + test(`• has correct 'prompt' metadata`, async () => { const test = createTest( URI.file('/absolute/folder/and/a/filename.txt'), @@ -972,7 +1017,7 @@ suite('TextModelPromptParser', () => { test.model.dispose(); assert( - test.parser.disposed, + test.parser.isDisposed, 'The parser should be disposed with its model.', ); }); diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts index c0422260939..00e9e6a1d14 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts @@ -77,7 +77,7 @@ class TestPromptFileReference extends Disposable { private readonly rootFileUri: URI, private readonly expectedReferences: ExpectedReference[], @IFileService private readonly fileService: IFileService, - @IInstantiationService private readonly initService: IInstantiationService, + @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); @@ -93,7 +93,7 @@ class TestPromptFileReference extends Disposable { options: Partial = {}, ): Promise { // create the files structure on the disk - await (this.initService.createInstance(MockFilesystem, this.fileStructure)).mock(); + await (this.instantiationService.createInstance(MockFilesystem, this.fileStructure)).mock(); // randomly test with and without delay to ensure that the file // reference resolution is not susceptible to race conditions @@ -103,7 +103,7 @@ class TestPromptFileReference extends Disposable { // start resolving references for the specified root file const rootReference = this._register( - this.initService.createInstance( + this.instantiationService.createInstance( FilePromptParser, this.rootFileUri, options, diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts index 948ed26b1cf..2802571e21c 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts @@ -158,7 +158,7 @@ suite('PromptsService', () => { ); assert( - !parser1.disposed, + !parser1.isDisposed, 'Parser1 must not be disposed.', ); @@ -233,7 +233,7 @@ suite('PromptsService', () => { ); assert( - !parser2.disposed, + !parser2.isDisposed, 'Parser2 must not be disposed.', ); @@ -243,17 +243,17 @@ suite('PromptsService', () => { ); assert( - !parser2.disposed, + !parser2.isDisposed, 'Parser2 must not be disposed.', ); assert( - !parser1.disposed, + !parser1.isDisposed, 'Parser1 must not be disposed.', ); assert( - !parser1_1.disposed, + !parser1_1.isDisposed, 'Parser1_1 must not be disposed.', ); @@ -314,17 +314,17 @@ suite('PromptsService', () => { parser1.dispose(); assert( - parser1.disposed, + parser1.isDisposed, 'Parser1 must be disposed.', ); assert( - parser1_1.disposed, + parser1_1.isDisposed, 'Parser1_1 must be disposed.', ); assert( - !parser2.disposed, + !parser2.isDisposed, 'Parser2 must not be disposed.', ); @@ -337,7 +337,7 @@ suite('PromptsService', () => { const parser1_2 = service.getSyntaxParserFor(model1); assert( - !parser1_2.disposed, + !parser1_2.isDisposed, 'Parser1_2 must not be disposed.', ); @@ -390,13 +390,13 @@ suite('PromptsService', () => { // assert that the parser is also disposed assert( - parser2.disposed, + parser2.isDisposed, 'Parser2 must be disposed.', ); // sanity check that the other parser is not affected assert( - !parser1_2.disposed, + !parser1_2.isDisposed, 'Parser1_2 must not be disposed.', ); @@ -416,7 +416,7 @@ suite('PromptsService', () => { const parser2_1 = service.getSyntaxParserFor(model2_1); assert( - !parser2_1.disposed, + !parser2_1.isDisposed, 'Parser2_1 must not be disposed.', ); @@ -472,7 +472,7 @@ suite('PromptsService', () => { // sanity checks assert( - !parser.disposed, + parser.isDisposed === false, 'Parser must not be disposed.', ); assert( diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/mockFilesystem.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/mockFilesystem.test.ts index b56531f5d19..05f1b5741f3 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/mockFilesystem.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/mockFilesystem.test.ts @@ -182,21 +182,21 @@ const validateFolder = async ( suite('MockFilesystem', () => { const disposables = ensureNoDisposablesAreLeakedInTestSuite(); - let initService: TestInstantiationService; + let instantiationService: TestInstantiationService; let fileService: IFileService; setup(async () => { - initService = disposables.add(new TestInstantiationService()); - initService.stub(ILogService, new NullLogService()); + instantiationService = disposables.add(new TestInstantiationService()); + instantiationService.stub(ILogService, new NullLogService()); - fileService = disposables.add(initService.createInstance(FileService)); + fileService = disposables.add(instantiationService.createInstance(FileService)); const fileSystemProvider = disposables.add(new InMemoryFileSystemProvider()); disposables.add(fileService.registerProvider(Schemas.file, fileSystemProvider)); - initService.stub(IFileService, fileService); + instantiationService.stub(IFileService, fileService); }); test('• mocks file structure', async () => { - const mockFilesystem = initService.createInstance(MockFilesystem, [ + const mockFilesystem = instantiationService.createInstance(MockFilesystem, [ { name: '/root/folder', children: [ diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/utils/promptFilesLocator.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/utils/promptFilesLocator.test.ts index 0300640e18e..27a776c4475 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/utils/promptFilesLocator.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/utils/promptFilesLocator.test.ts @@ -63,16 +63,16 @@ suite('PromptFilesLocator', () => { return; } - let initService: TestInstantiationService; + let instantiationService: TestInstantiationService; setup(async () => { - initService = disposables.add(new TestInstantiationService()); - initService.stub(ILogService, new NullLogService()); + instantiationService = disposables.add(new TestInstantiationService()); + instantiationService.stub(ILogService, new NullLogService()); - const fileService = disposables.add(initService.createInstance(FileService)); + const fileService = disposables.add(instantiationService.createInstance(FileService)); const fileSystemProvider = disposables.add(new InMemoryFileSystemProvider()); disposables.add(fileService.registerProvider(Schemas.file, fileSystemProvider)); - initService.stub(IFileService, fileService); + instantiationService.stub(IFileService, fileService); }); /** @@ -84,9 +84,9 @@ suite('PromptFilesLocator', () => { workspaceFolderPaths: string[], filesystem: IMockFolder[], ): Promise => { - await (initService.createInstance(MockFilesystem, filesystem)).mock(); + await (instantiationService.createInstance(MockFilesystem, filesystem)).mock(); - initService.stub(IConfigurationService, mockConfigService(configValue)); + instantiationService.stub(IConfigurationService, mockConfigService(configValue)); const workspaceFolders = workspaceFolderPaths.map((path, index) => { const uri = createURI(path); @@ -97,9 +97,9 @@ suite('PromptFilesLocator', () => { index, }); }); - initService.stub(IWorkspaceContextService, mockWorkspaceService(workspaceFolders)); + instantiationService.stub(IWorkspaceContextService, mockWorkspaceService(workspaceFolders)); - return initService.createInstance(PromptFilesLocator); + return instantiationService.createInstance(PromptFilesLocator); }; suite('• empty workspace', () => { diff --git a/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts b/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts index c8f8acd1fab..6213fc4f3ca 100644 --- a/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts @@ -8,7 +8,6 @@ import { CancellationToken, CancellationTokenSource } from '../../../../../base/ import { Emitter, Event } from '../../../../../base/common/event.js'; import { DisposableStore, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; -import { ProviderResult } from '../../../../../editor/common/languages.js'; import { ExtensionIdentifier } from '../../../../../platform/extensions/common/extensions.js'; import { MockContextKeyService } from '../../../../../platform/keybinding/test/common/mockKeybindingService.js'; import { nullExtensionDescription } from '../../../../services/extensions/common/extensions.js'; @@ -47,9 +46,6 @@ suite('VoiceChat', () => { provideFollowups?(request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { throw new Error('Method not implemented.'); } - provideSampleQuestions?(location: ChatAgentLocation, token: CancellationToken): ProviderResult { - throw new Error('Method not implemented.'); - } invoke(request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { throw new Error('Method not implemented.'); } metadata = {}; } diff --git a/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts b/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts index 6459f64e622..aa18d550d2a 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts @@ -33,6 +33,7 @@ import { IContextMenuService } from '../../../../../platform/contextview/browser import { StandardMouseEvent } from '../../../../../base/browser/mouseEvent.js'; import { ChatAgentLocation } from '../../../chat/common/constants.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { Position } from '../../../../../editor/common/core/position.js'; export const emptyTextEditorHintSetting = 'workbench.editor.empty.hint'; export class EmptyTextEditorHintContribution extends Disposable implements IEditorContribution { @@ -319,6 +320,8 @@ class EmptyTextEditorHintContentWidget extends Disposable implements IContentWid })); this.editor.applyFontInfo(this.domNode); + const lineHeight = this.editor.getLineHeightForPosition(new Position(1, 1)); + this.domNode.style.lineHeight = lineHeight + 'px'; } return this.domNode; diff --git a/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts b/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts index efa3cd9fa68..ea9f5e7949f 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts @@ -87,7 +87,7 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi private input!: IActiveCodeEditor; private selectBreakpointBox!: SelectBox; private selectModeBox?: SelectBox; - private toDispose: lifecycle.IDisposable[]; + private store: lifecycle.DisposableStore; private conditionInput = ''; private hitCountInput = ''; private logMessageInput = ''; @@ -113,7 +113,7 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi ) { super(editor, { showFrame: true, showArrow: false, frameWidth: 1, isAccessible: true }); - this.toDispose = []; + this.store = new lifecycle.DisposableStore(); const model = this.editor.getModel(); if (model) { const uri = model.uri; @@ -135,7 +135,7 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi this.context = context; } - this.toDispose.push(this.debugService.getModel().onDidChangeBreakpoints(e => { + this.store.add(this.debugService.getModel().onDidChangeBreakpoints(e => { if (this.breakpoint && e && e.removed && e.removed.indexOf(this.breakpoint) >= 0) { this.dispose(); } @@ -205,12 +205,12 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi protected _fillContainer(container: HTMLElement): void { this.setCssClass('breakpoint-widget'); - const selectBox = new SelectBox([ + const selectBox = this.store.add(new SelectBox([ { text: nls.localize('expression', "Expression") }, { text: nls.localize('hitCount', "Hit Count") }, { text: nls.localize('logMessage', "Log Message") }, { text: nls.localize('triggeredBy', "Wait for Breakpoint") }, - ] satisfies ISelectOptionItem[], this.context, this.contextViewService, defaultSelectBoxStyles, { ariaLabel: nls.localize('breakpointType', 'Breakpoint Type') }); + ] satisfies ISelectOptionItem[], this.context, this.contextViewService, defaultSelectBoxStyles, { ariaLabel: nls.localize('breakpointType', 'Breakpoint Type') })); this.selectContainer = $('.breakpoint-select-container'); selectBox.render(dom.append(container, this.selectContainer)); selectBox.onDidSelect(e => { @@ -222,11 +222,11 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi this.createModesInput(container); this.inputContainer = $('.inputContainer'); - this.toDispose.push(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.inputContainer, this.placeholder)); + this.store.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.inputContainer, this.placeholder)); this.createBreakpointInput(dom.append(container, this.inputContainer)); this.input.getModel().setValue(this.getInputValue(this.breakpoint)); - this.toDispose.push(this.input.getModel().onDidChangeContent(() => { + this.store.add(this.input.getModel().onDidChangeContent(() => { this.fitHeightToContent(); })); this.input.setPosition({ lineNumber: 1, column: this.input.getModel().getLineMaxColumn(1) }); @@ -253,8 +253,8 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi this.contextViewService, defaultSelectBoxStyles, ); - this.toDispose.push(sb); - this.toDispose.push(sb.onDidSelect(e => { + this.store.add(sb); + this.store.add(sb.onDidSelect(e => { this.modeInput = modes[e.index - 1]; })); @@ -296,9 +296,9 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi this.triggeredByBreakpointInput = breakpoints[e.index - 1]; } }); - this.toDispose.push(selectBreakpointBox); + this.store.add(selectBreakpointBox); this.selectBreakpointContainer = $('.select-breakpoint-container'); - this.toDispose.push(dom.addDisposableListener(this.selectBreakpointContainer, dom.EventType.KEY_DOWN, e => { + this.store.add(dom.addDisposableListener(this.selectBreakpointContainer, dom.EventType.KEY_DOWN, e => { const event = new StandardKeyboardEvent(e); if (event.equals(KeyCode.Escape)) { this.close(false); @@ -313,8 +313,8 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi const closeButton = new Button(this.selectBreakpointContainer, defaultButtonStyles); closeButton.label = nls.localize('ok', "OK"); - this.toDispose.push(closeButton.onDidClick(() => this.close(true))); - this.toDispose.push(closeButton); + this.store.add(closeButton.onDidClick(() => this.close(true))); + this.store.add(closeButton); } private updateContextInput() { @@ -347,7 +347,7 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi const scopedInstatiationService = this.instantiationService.createChild(new ServiceCollection( [IPrivateBreakpointWidgetService, this] )); - this.toDispose.push(scopedInstatiationService); + this.store.add(scopedInstatiationService); const options = this.createEditorOptions(); const codeEditorWidgetOptions = getSimpleCodeEditorWidgetOptions(); @@ -360,7 +360,7 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi } this.input.setModel(model); this.setInputMode(); - this.toDispose.push(model); + this.store.add(model); const setDecorations = () => { const value = this.input.getModel().getValue(); const decorations = !!value ? [] : createDecorations(this.themeService.getColorTheme(), this.placeholder); @@ -369,7 +369,7 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi this.input.getModel().onDidChangeContent(() => setDecorations()); this.themeService.onDidColorThemeChange(() => setDecorations()); - this.toDispose.push(this.languageFeaturesService.completionProvider.register({ scheme: DEBUG_SCHEME, hasAccessToAllModels: true }, { + this.store.add(this.languageFeaturesService.completionProvider.register({ scheme: DEBUG_SCHEME, hasAccessToAllModels: true }, { _debugDisplayName: 'breakpointWidget', provideCompletionItems: (model: ITextModel, position: Position, _context: CompletionContext, token: CancellationToken): Promise => { let suggestionsPromise: Promise; @@ -403,7 +403,7 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi } })); - this.toDispose.push(this._configurationService.onDidChangeConfiguration((e) => { + this.store.add(this._configurationService.onDidChangeConfiguration((e) => { if (e.affectsConfiguration('editor.fontSize') || e.affectsConfiguration('editor.lineHeight')) { this.input.updateOptions(this.createEditorOptions()); this.centerInputVertically(); @@ -508,7 +508,7 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi override dispose(): void { super.dispose(); this.input.dispose(); - lifecycle.dispose(this.toDispose); + lifecycle.dispose(this.store); setTimeout(() => this.editor.focus(), 0); } } diff --git a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts index fd9120ac2d7..255c41c225d 100644 --- a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts @@ -599,6 +599,11 @@ configurationRegistry.registerConfiguration({ description: nls.localize('debug.console.acceptSuggestionOnEnter', "Controls whether suggestions should be accepted on Enter in the Debug Console. Enter is also used to evaluate whatever is typed in the Debug Console."), default: 'off' }, + 'debug.console.maximumLines': { + type: 'number', + description: nls.localize('debug.console.maximumLines', "Controls the maximum number of lines in the Debug Console."), + default: 10000 + }, 'launch': { type: 'object', description: nls.localize({ comment: ['This is the description for a setting'], key: 'launch' }, "Global debug launch configuration. Should be used as an alternative to 'launch.json' that is shared across workspaces."), diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index 7054f7ad505..0d5522e3a52 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -26,6 +26,7 @@ import { IAccessibilityService } from '../../../../platform/accessibility/common import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../../platform/log/common/log.js'; +import { FocusMode } from '../../../../platform/native/common/native.js'; import { INotificationService } from '../../../../platform/notification/common/notification.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; import { ICustomEndpointTelemetryService, ITelemetryService, TelemetryLevel } from '../../../../platform/telemetry/common/telemetry.js'; @@ -1375,7 +1376,7 @@ export class DebugSession implements IDebugSession { if (this.configurationService.getValue('debug').focusWindowOnBreak && !this.workbenchEnvironmentService.extensionTestsLocationURI) { const activeWindow = getActiveWindow(); if (!activeWindow.document.hasFocus()) { - await this.hostService.focus(mainWindow, { force: true /* Application may not be active */ }); + await this.hostService.focus(mainWindow, { mode: FocusMode.Force /* Application may not be active */ }); } } } diff --git a/src/vs/workbench/contrib/debug/browser/linkDetector.ts b/src/vs/workbench/contrib/debug/browser/linkDetector.ts index 8d08e6f1be9..6e365eabb52 100644 --- a/src/vs/workbench/contrib/debug/browser/linkDetector.ts +++ b/src/vs/workbench/contrib/debug/browser/linkDetector.ts @@ -30,9 +30,9 @@ const CONTROL_CODES = '\\u0000-\\u0020\\u007f-\\u009f'; const WEB_LINK_REGEX = new RegExp('(?:[a-zA-Z][a-zA-Z0-9+.-]{2,}:\\/\\/|data:|www\\.)[^\\s' + CONTROL_CODES + '"]{2,}[^\\s' + CONTROL_CODES + '"\')}\\],:;.!?]', 'ug'); const WIN_ABSOLUTE_PATH = /(?:[a-zA-Z]:(?:(?:\\|\/)[\w\.-]*)+)/; -const WIN_RELATIVE_PATH = /(?:(?:\~|\.)(?:(?:\\|\/)[\w\.-]*)+)/; +const WIN_RELATIVE_PATH = /(?:(?:\~|\.+)(?:(?:\\|\/)[\w\.-]*)+)/; const WIN_PATH = new RegExp(`(${WIN_ABSOLUTE_PATH.source}|${WIN_RELATIVE_PATH.source})`); -const POSIX_PATH = /((?:\~|\.)?(?:\/[\w\.-]*)+)/; +const POSIX_PATH = /((?:\~|\.+)?(?:\/[\w\.-]*)+)/; const LINE_COLUMN = /(?:\:([\d]+))?(?:\:([\d]+))?/; const PATH_LINK_REGEX = new RegExp(`${platform.isWindows ? WIN_PATH.source : POSIX_PATH.source}${LINE_COLUMN.source}`, 'g'); const LINE_COLUMN_REGEX = /:([\d]+)(?::([\d]+))?$/; diff --git a/src/vs/workbench/contrib/debug/browser/welcomeView.ts b/src/vs/workbench/contrib/debug/browser/welcomeView.ts index ebb161fc785..b65402ed59b 100644 --- a/src/vs/workbench/contrib/debug/browser/welcomeView.ts +++ b/src/vs/workbench/contrib/debug/browser/welcomeView.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { DisposableStore } from '../../../../base/common/lifecycle.js'; -import { isMacintosh, isWeb } from '../../../../base/common/platform.js'; import { isCodeEditor, isDiffEditor } from '../../../../editor/browser/editorBrowser.js'; import { localize, localize2 } from '../../../../nls.js'; import { ILocalizedString } from '../../../../platform/action/common/action.js'; @@ -18,7 +17,7 @@ import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; -import { OpenFileAction, OpenFileFolderAction, OpenFolderAction } from '../../../browser/actions/workspaceActions.js'; +import { OpenFileAction, OpenFolderAction } from '../../../browser/actions/workspaceActions.js'; import { ViewPane } from '../../../browser/parts/views/viewPane.js'; import { IViewletViewOptions } from '../../../browser/parts/views/viewsViewlet.js'; import { WorkbenchStateContext } from '../../../common/contextkeys.js'; @@ -124,7 +123,7 @@ viewsRegistry.registerViewWelcomeContent(WelcomeView.ID, { '{Locked="](command:{0})"}' ] }, - "[Open a file](command:{0}) which can be debugged or run.", (isMacintosh && !isWeb) ? OpenFileFolderAction.ID : OpenFileAction.ID + "[Open a file](command:{0}) which can be debugged or run.", OpenFileAction.ID ), when: ContextKeyExpr.and(CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_DEBUGGER_INTERESTED_IN_ACTIVE_EDITOR.toNegated()), group: ViewContentGroups.Open, @@ -163,7 +162,7 @@ viewsRegistry.registerViewWelcomeContent(WelcomeView.ID, { '{Locked="](command:{0})"}', ] }, - "To customize Run and Debug, [open a folder](command:{0}) and create a launch.json file.", (isMacintosh && !isWeb) ? OpenFileFolderAction.ID : OpenFolderAction.ID), + "To customize Run and Debug, [open a folder](command:{0}) and create a launch.json file.", OpenFolderAction.ID), when: ContextKeyExpr.and(CONTEXT_DEBUGGERS_AVAILABLE, WorkbenchStateContext.isEqualTo('empty')), group: ViewContentGroups.Debug }); diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index 545504bfde0..a2b1a8b2c6b 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -809,6 +809,7 @@ export interface IDebugConfiguration { collapseIdenticalLines: boolean; historySuggestions: boolean; acceptSuggestionOnEnter: 'off' | 'on'; + maximumLines: number; }; focusWindowOnBreak: boolean; focusEditorOnBreak: boolean; diff --git a/src/vs/workbench/contrib/debug/common/replModel.ts b/src/vs/workbench/contrib/debug/common/replModel.ts index e2e388eed51..244edd90027 100644 --- a/src/vs/workbench/contrib/debug/common/replModel.ts +++ b/src/vs/workbench/contrib/debug/common/replModel.ts @@ -12,7 +12,6 @@ import { IConfigurationService } from '../../../../platform/configuration/common import { IDebugConfiguration, IDebugSession, IExpression, INestingReplElement, IReplElement, IReplElementSource, IStackFrame } from './debug.js'; import { ExpressionContainer } from './debugModel.js'; -const MAX_REPL_LENGTH = 10000; let topReplElementCounter = 0; const getUniqueId = () => `topReplElement:${topReplElementCounter++}`; @@ -343,8 +342,9 @@ export class ReplModel { lastElement.addChild(newElement); } else { this.replElements.push(newElement); - if (this.replElements.length > MAX_REPL_LENGTH) { - this.replElements.splice(0, this.replElements.length - MAX_REPL_LENGTH); + const config = this.configurationService.getValue('debug'); + if (this.replElements.length > config.console.maximumLines) { + this.replElements.splice(0, this.replElements.length - config.console.maximumLines); } } this._onDidChangeElements.fire(newElement); diff --git a/src/vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts b/src/vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts index 1bb5eb33737..ce109ad4f29 100644 --- a/src/vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts +++ b/src/vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts @@ -35,7 +35,7 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes public readonly SIZE_LIMIT = Math.floor(1024 * 1024 * 1.9); // 2 MB - private serverConfiguration = this.productService['editSessions.store']; + private serverConfiguration; private machineClient: IUserDataSyncMachinesService | undefined; private authenticationInfo: { sessionId: string; token: string; providerId: string } | undefined; @@ -84,7 +84,7 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes @ISecretStorageService private readonly secretStorageService: ISecretStorageService ) { super(); - + this.serverConfiguration = this.productService['editSessions.store']; // If the user signs out of the current session, reset our cached auth state in memory and on disk this._register(this.authenticationService.onDidChangeSessions((e) => this.onDidChangeSessions(e.event))); diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index 8032b3615d9..9e80f042578 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -13,11 +13,11 @@ import { EnablementState, IExtensionManagementServerService, IPublisherInfo, IWo import { IExtensionIgnoredRecommendationsService, IExtensionRecommendationsService } from '../../../services/extensionRecommendations/common/extensionRecommendations.js'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; -import { VIEWLET_ID, IExtensionsWorkbenchService, IExtensionsViewPaneContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, WORKSPACE_RECOMMENDATIONS_VIEW_ID, IWorkspaceRecommendedExtensionsView, AutoUpdateConfigurationKey, HasOutdatedExtensionsContext, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID, LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID, ExtensionEditorTab, THEME_ACTIONS_GROUP, INSTALL_ACTIONS_GROUP, OUTDATED_EXTENSIONS_VIEW_ID, CONTEXT_HAS_GALLERY, extensionsSearchActionsMenu, UPDATE_ACTIONS_GROUP, IExtensionArg, ExtensionRuntimeActionType, EXTENSIONS_CATEGORY, AutoRestartConfigurationKey } from '../common/extensions.js'; +import { VIEWLET_ID, IExtensionsWorkbenchService, IExtensionsViewPaneContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, WORKSPACE_RECOMMENDATIONS_VIEW_ID, IWorkspaceRecommendedExtensionsView, AutoUpdateConfigurationKey, HasOutdatedExtensionsContext, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID, LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID, ExtensionEditorTab, THEME_ACTIONS_GROUP, INSTALL_ACTIONS_GROUP, OUTDATED_EXTENSIONS_VIEW_ID, CONTEXT_HAS_GALLERY, extensionsSearchActionsMenu, UPDATE_ACTIONS_GROUP, IExtensionArg, ExtensionRuntimeActionType, EXTENSIONS_CATEGORY, AutoRestartConfigurationKey, extensionsFilterSubMenu, DefaultViewsContext } from '../common/extensions.js'; import { InstallSpecificVersionOfExtensionAction, ConfigureWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction, SetColorThemeAction, SetFileIconThemeAction, SetProductIconThemeAction, ClearLanguageAction, ToggleAutoUpdateForExtensionAction, ToggleAutoUpdatesForPublisherAction, TogglePreReleaseExtensionAction, InstallAnotherVersionAction, InstallAction } from './extensionsActions.js'; import { ExtensionsInput } from '../common/extensionsInput.js'; import { ExtensionEditor } from './extensionEditor.js'; -import { StatusUpdater, MaliciousExtensionChecker, ExtensionsViewletViewsContribution, ExtensionsViewPaneContainer, BuiltInExtensionsContext, SearchMarketplaceExtensionsContext, RecommendedExtensionsContext, DefaultViewsContext, ExtensionsSortByContext, SearchHasTextContext, ExtensionsSearchValueContext } from './extensionsViewlet.js'; +import { StatusUpdater, MaliciousExtensionChecker, ExtensionsViewletViewsContribution, ExtensionsViewPaneContainer, BuiltInExtensionsContext, SearchMarketplaceExtensionsContext, RecommendedExtensionsContext, ExtensionsSortByContext, SearchHasTextContext, ExtensionsSearchValueContext } from './extensionsViewlet.js'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from '../../../../platform/configuration/common/configurationRegistry.js'; import * as jsonContributionRegistry from '../../../../platform/jsonschemas/common/jsonContributionRegistry.js'; import { ExtensionsConfigurationSchema, ExtensionsConfigurationSchemaId } from '../common/extensionsFileTemplate.js'; @@ -108,7 +108,7 @@ Registry.as(EditorExtensions.EditorPane).registerEditorPane new SyncDescriptor(ExtensionsInput) ]); -Registry.as(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer( +export const VIEW_CONTAINER = Registry.as(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer( { id: VIEWLET_ID, title: localize2('extensions', "Extensions"), @@ -949,7 +949,6 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi } }); - const extensionsFilterSubMenu = new MenuId('extensionsFilterSubMenu'); MenuRegistry.appendMenuItem(extensionsSearchActionsMenu, { submenu: extensionsFilterSubMenu, title: localize('filterExtensions', "Filter Extensions..."), diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 862d00b9fd8..5c24f2dd128 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -1211,7 +1211,7 @@ export abstract class DropDownExtensionAction extends ExtensionAction { export class DropDownExtensionActionViewItem extends ActionViewItem { constructor( - action: DropDownExtensionAction, + action: IAction, options: IActionViewItemOptions, @IContextMenuService private readonly contextMenuService: IContextMenuService ) { diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsIcons.ts b/src/vs/workbench/contrib/extensions/browser/extensionsIcons.ts index ea1bc04ede8..04280117b3b 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsIcons.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsIcons.ts @@ -4,7 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { Codicon } from '../../../../base/common/codicons.js'; +import { Color, RGBA } from '../../../../base/common/color.js'; import { localize } from '../../../../nls.js'; +import { contrastBorder, registerColor } from '../../../../platform/theme/common/colorRegistry.js'; import { registerIcon } from '../../../../platform/theme/common/iconRegistry.js'; export const extensionsViewIcon = registerIcon('extensions-view-icon', Codicon.extensions, localize('extensionsViewIcon', 'View icon of the extensions view.')); @@ -38,3 +40,9 @@ export const infoIcon = registerIcon('extensions-info-message', Codicon.info, lo export const trustIcon = registerIcon('extension-workspace-trust', Codicon.shield, localize('trustIcon', 'Icon shown with a workspace trust message in the extension editor.')); export const activationTimeIcon = registerIcon('extension-activation-time', Codicon.history, localize('activationtimeIcon', 'Icon shown with a activation time message in the extension editor.')); + +export const extensionBorder = registerColor( + 'extension.border', + { dark: new Color(new RGBA(255, 255, 255, 0.10)), light: new Color(new RGBA(0, 0, 0, 0.10)), hcDark: contrastBorder, hcLight: contrastBorder, }, + localize('extension.border', 'The border color of an extension.') +); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts index d7c6d3c70be..1ee67d0e3c5 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts @@ -16,7 +16,7 @@ import { append, $, Dimension, hide, show, DragAndDropObserver, trackFocus, addD import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; -import { IExtensionsWorkbenchService, IExtensionsViewPaneContainer, VIEWLET_ID, CloseExtensionDetailsOnViewChangeKey, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, WORKSPACE_RECOMMENDATIONS_VIEW_ID, AutoCheckUpdatesConfigurationKey, OUTDATED_EXTENSIONS_VIEW_ID, CONTEXT_HAS_GALLERY, extensionsSearchActionsMenu, AutoRestartConfigurationKey, ExtensionRuntimeActionType } from '../common/extensions.js'; +import { IExtensionsWorkbenchService, IExtensionsViewPaneContainer, VIEWLET_ID, CloseExtensionDetailsOnViewChangeKey, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, WORKSPACE_RECOMMENDATIONS_VIEW_ID, AutoCheckUpdatesConfigurationKey, OUTDATED_EXTENSIONS_VIEW_ID, CONTEXT_HAS_GALLERY, extensionsSearchActionsMenu, AutoRestartConfigurationKey, ExtensionRuntimeActionType, SearchMcpServersContext, DefaultViewsContext } from '../common/extensions.js'; import { InstallLocalExtensionsInRemoteAction, InstallRemoteExtensionsInLocalAction } from './extensionsActions.js'; import { IExtensionManagementService, ILocalExtension } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { IWorkbenchExtensionEnablementService, IExtensionManagementServerService, IExtensionManagementServer } from '../../../services/extensionManagement/common/extensionManagement.js'; @@ -69,8 +69,8 @@ import { ThemeIcon } from '../../../../base/common/themables.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { IExtensionGalleryManifest, IExtensionGalleryManifestService } from '../../../../platform/extensionManagement/common/extensionGalleryManifest.js'; import { URI } from '../../../../base/common/uri.js'; +import { IMcpGalleryService } from '../../../../platform/mcp/common/mcpManagement.js'; -export const DefaultViewsContext = new RawContextKey('defaultExtensionViews', true); export const ExtensionsSortByContext = new RawContextKey('extensionsSortByValue', ''); export const SearchMarketplaceExtensionsContext = new RawContextKey('searchMarketplaceExtensions', false); export const SearchHasTextContext = new RawContextKey('extensionSearchHasText', false); @@ -483,6 +483,7 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE private readonly defaultViewsContextKey: IContextKey; private readonly sortByContextKey: IContextKey; private readonly searchMarketplaceExtensionsContextKey: IContextKey; + private readonly searchMcpServersContextKey: IContextKey; private readonly searchHasTextContextKey: IContextKey; private readonly sortByUpdateDateContextKey: IContextKey; private readonly installedExtensionsContextKey: IContextKey; @@ -528,6 +529,7 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE @IViewDescriptorService viewDescriptorService: IViewDescriptorService, @IPreferencesService private readonly preferencesService: IPreferencesService, @ICommandService private readonly commandService: ICommandService, + @IMcpGalleryService private readonly mcpGalleryService: IMcpGalleryService, @ILogService logService: ILogService, ) { super(VIEWLET_ID, { mergeViewWithContainerWhenSingleView: true }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService, viewDescriptorService, logService); @@ -537,6 +539,7 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE this.defaultViewsContextKey = DefaultViewsContext.bindTo(contextKeyService); this.sortByContextKey = ExtensionsSortByContext.bindTo(contextKeyService); this.searchMarketplaceExtensionsContextKey = SearchMarketplaceExtensionsContext.bindTo(contextKeyService); + this.searchMcpServersContextKey = SearchMcpServersContext.bindTo(contextKeyService); this.searchHasTextContextKey = SearchHasTextContext.bindTo(contextKeyService); this.sortByUpdateDateContextKey = SortByUpdateDateContext.bindTo(contextKeyService); this.installedExtensionsContextKey = InstalledExtensionsContext.bindTo(contextKeyService); @@ -819,7 +822,8 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE this.searchDeprecatedExtensionsContextKey.set(ExtensionsListView.isSearchDeprecatedExtensionsQuery(value)); this.builtInExtensionsContextKey.set(ExtensionsListView.isBuiltInExtensionsQuery(value)); this.recommendedExtensionsContextKey.set(isRecommendedExtensionsQuery); - this.searchMarketplaceExtensionsContextKey.set(!!value && !ExtensionsListView.isLocalExtensionsQuery(value) && !isRecommendedExtensionsQuery); + this.searchMcpServersContextKey.set(this.mcpGalleryService.isEnabled() && !!value && /@mcp\s?.*/i.test(value)); + this.searchMarketplaceExtensionsContextKey.set(!!value && !ExtensionsListView.isLocalExtensionsQuery(value) && !isRecommendedExtensionsQuery && !this.searchMcpServersContextKey.get()); this.sortByUpdateDateContextKey.set(ExtensionsListView.isSortUpdateDateQuery(value)); this.defaultViewsContextKey.set(!value || ExtensionsListView.isSortInstalledExtensionsQuery(value)); }); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index 367c7ee8ca9..e4b2022d6ce 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -2104,7 +2104,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } if (autoUpdateValue === 'onlyEnabledExtensions') { - return this.extensionEnablementService.isEnabledEnablementState(extension.enablementState); + return extension.enablementState !== EnablementState.DisabledGlobally && extension.enablementState !== EnablementState.DisabledWorkspace; } return false; diff --git a/src/vs/workbench/contrib/extensions/browser/media/extension.css b/src/vs/workbench/contrib/extensions/browser/media/extension.css index df7aaffb5e9..b382ca89b44 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extension.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extension.css @@ -34,6 +34,11 @@ object-fit: contain; } +.extension-list-item > .icon-container > .codicon { + padding-right: 14px; + font-size: 42px !important; +} + .extension-list-item > .icon-container .extension-badge { position: absolute; bottom: 5px; diff --git a/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css b/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css index 12ea3c206aa..9ef880d8ada 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css @@ -36,6 +36,10 @@ object-fit: contain; } +.extension-editor > .header > .icon-container > .codicon { + font-size: 128px !important; +} + .extension-editor > .header > .icon-container .extension-remote-badge { position: absolute; right: 0px; diff --git a/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css b/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css index 266478a3815..4629bd09c7b 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css @@ -168,10 +168,14 @@ height: 24px; padding-right: 8px; } +.extensions-viewlet.narrow > .extensions .extension-list-item > .icon-container > .codicon { + font-size: 24px !important; + padding-right: 8px; +} .extensions-viewlet:not(.narrow) > .extensions .extension-list-item > .details > .header-container > .header > .extension-remote-badge-container, .extensions-viewlet.narrow > .extensions .extension-list-item > .icon-container .extension-badge, -.extensions-viewlet.mini > .extensions .extension-list-item > .icon-container > .icon, +.extensions-viewlet.mini > .extensions .extension-list-item > .icon-container, .extensions-viewlet.mini > .extensions .extension-list-item > .details > .header-container > .header > .ratings, .extensions-viewlet.mini > .extensions .extension-bookmark-container { display: none; diff --git a/src/vs/workbench/contrib/extensions/common/extensions.ts b/src/vs/workbench/contrib/extensions/common/extensions.ts index ff3b6412d43..6826b7e0b3d 100644 --- a/src/vs/workbench/contrib/extensions/common/extensions.ts +++ b/src/vs/workbench/contrib/extensions/common/extensions.ts @@ -250,9 +250,11 @@ export const INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID = 'workbench.extensions.comm export const LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID = 'workbench.extensions.action.listWorkspaceUnsupportedExtensions'; // Context Keys +export const DefaultViewsContext = new RawContextKey('defaultExtensionViews', true); export const HasOutdatedExtensionsContext = new RawContextKey('hasOutdatedExtensions', false); export const CONTEXT_HAS_GALLERY = new RawContextKey('hasGallery', false); export const ExtensionResultsListFocused = new RawContextKey('extensionResultListFocused ', true); +export const SearchMcpServersContext = new RawContextKey('searchMcpServers', false); // Context Menu Groups export const THEME_ACTIONS_GROUP = '_theme_'; @@ -260,6 +262,7 @@ export const INSTALL_ACTIONS_GROUP = '0_install'; export const UPDATE_ACTIONS_GROUP = '0_update'; export const extensionsSearchActionsMenu = new MenuId('extensionsSearchActionsMenu'); +export const extensionsFilterSubMenu = new MenuId('extensionsFilterSubMenu'); export interface IExtensionArg { id: string; diff --git a/src/vs/workbench/contrib/extensions/common/searchExtensionsTool.ts b/src/vs/workbench/contrib/extensions/common/searchExtensionsTool.ts index ed51f09611c..014ae6079b8 100644 --- a/src/vs/workbench/contrib/extensions/common/searchExtensionsTool.ts +++ b/src/vs/workbench/contrib/extensions/common/searchExtensionsTool.ts @@ -19,7 +19,6 @@ export const SearchExtensionsToolData: IToolData = { toolReferenceName: 'extensions', canBeReferencedInPrompt: true, icon: ThemeIcon.fromId(Codicon.extensions.id), - supportsToolPicker: true, displayName: localize('searchExtensionsTool.displayName', 'Search Extensions'), modelDescription: localize('searchExtensionsTool.modelDescription', "This is a tool for browsing Visual Studio Code Extensions Marketplace. It allows the model to search for extensions and retrieve detailed information about them. The model should use this tool whenever it needs to discover extensions or resolve information about known ones. To use the tool, the model has to provide the category of the extensions, relevant search keywords, or known extension IDs. Note that search results may include false positives, so reviewing and filtering is recommended."), userDescription: localize('searchExtensionsTool.userDescription', 'Search for extensions in the Visual Studio Code Extensions Marketplace.'), diff --git a/src/vs/workbench/contrib/files/browser/explorerViewlet.ts b/src/vs/workbench/contrib/files/browser/explorerViewlet.ts index 48d50144a3b..80b38b619e3 100644 --- a/src/vs/workbench/contrib/files/browser/explorerViewlet.ts +++ b/src/vs/workbench/contrib/files/browser/explorerViewlet.ts @@ -32,9 +32,8 @@ import { IProgressService, ProgressLocation } from '../../../../platform/progres import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; import { WorkbenchStateContext, RemoteNameContext, OpenFolderWorkspaceSupportContext } from '../../../common/contextkeys.js'; import { IsWebContext } from '../../../../platform/contextkey/common/contextkeys.js'; -import { AddRootFolderAction, OpenFolderAction, OpenFileFolderAction, OpenFolderViaWorkspaceAction } from '../../../browser/actions/workspaceActions.js'; +import { AddRootFolderAction, OpenFolderAction, OpenFolderViaWorkspaceAction } from '../../../browser/actions/workspaceActions.js'; import { OpenRecentAction } from '../../../browser/actions/windowActions.js'; -import { isMacintosh, isWeb } from '../../../../base/common/platform.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { registerIcon } from '../../../../platform/theme/common/iconRegistry.js'; import { isMouseEvent } from '../../../../base/browser/dom.js'; @@ -279,7 +278,7 @@ const openRecent = localize('openRecent', "Open Recent"); const addRootFolderButton = `[${openFolder}](command:${AddRootFolderAction.ID})`; const addAFolderButton = `[${addAFolder}](command:${AddRootFolderAction.ID})`; -const openFolderButton = `[${openFolder}](command:${(isMacintosh && !isWeb) ? OpenFileFolderAction.ID : OpenFolderAction.ID})`; +const openFolderButton = `[${openFolder}](command:${OpenFolderAction.ID})`; const openFolderViaWorkspaceButton = `[${openFolder}](command:${OpenFolderViaWorkspaceAction.ID})`; const openRecentButton = `[${openRecent}](command:${OpenRecentAction.ID})`; diff --git a/src/vs/workbench/contrib/files/browser/fileImportExport.ts b/src/vs/workbench/contrib/files/browser/fileImportExport.ts index 44ecb6d65e5..0cc1fa6240f 100644 --- a/src/vs/workbench/contrib/files/browser/fileImportExport.ts +++ b/src/vs/workbench/contrib/files/browser/fileImportExport.ts @@ -718,7 +718,7 @@ export class FileDownload { listenStream(sourceStream, { onData: data => { - target.write(data.buffer); + target.write(data.buffer as Uint8Array); this.reportProgress(contents.name, contents.size, data.byteLength, operation); }, onError: error => { @@ -736,7 +736,7 @@ export class FileDownload { private async downloadFileUnbufferedBrowser(resource: URI, target: FileSystemWritableFileStream, operation: IDownloadOperation, token: CancellationToken): Promise { const contents = await this.fileService.readFile(resource, undefined, token); if (!token.isCancellationRequested) { - target.write(contents.value.buffer); + target.write(contents.value.buffer as Uint8Array); this.reportProgress(contents.name, contents.size, contents.value.byteLength, operation); } diff --git a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts index 9446c1ddd45..b46e3b12fe2 100644 --- a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts +++ b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts @@ -638,8 +638,8 @@ class EditorGroupRenderer implements IListRenderer { static readonly ID = 'openeditor'; - private readonly closeEditorAction = this.instantiationService.createInstance(CloseEditorAction, CloseEditorAction.ID, CloseEditorAction.LABEL); - private readonly unpinEditorAction = this.instantiationService.createInstance(UnpinEditorAction, UnpinEditorAction.ID, UnpinEditorAction.LABEL); + private readonly closeEditorAction; + private readonly unpinEditorAction; constructor( private labels: ResourceLabels, @@ -647,6 +647,8 @@ class OpenEditorRenderer implements IListRenderer('inlineChatShowingHint', false, localize('inlineChatShowingHint', "Whether inline chat shows a contextual hint")); @@ -224,7 +235,7 @@ export class InlineChatHintsController extends Disposable implements IEditorCont const configHintEmpty = observableConfigValue(InlineChatConfigKeys.LineEmptyHint, false, this._configurationService); const configHintNL = observableConfigValue(InlineChatConfigKeys.LineNLHint, false, this._configurationService); - const showDataObs = derivedWithStore((r, store) => { + const showDataObs = derived((r) => { const ghostState = ghostCtrl?.model.read(r)?.state.read(r); const textFocus = editorObs.isTextFocused.read(r); @@ -237,15 +248,14 @@ export class InlineChatHintsController extends Disposable implements IEditorCont return undefined; } - if (model.getLanguageId() === PLAINTEXT_LANGUAGE_ID || model.getLanguageId() === 'markdown' || model.getLanguageId() === 'search-result') { + if (IGNORED_LANGUAGE_IDS.has(model.getLanguageId())) { return undefined; } - // DEBT - I cannot use `model.onDidChangeContent` directly here // https://github.com/microsoft/vscode/issues/242059 - const emitter = store.add(new Emitter()); - store.add(model.onDidChangeContent(() => emitter.fire())); + const emitter = r.store.add(new Emitter()); + r.store.add(model.onDidChangeContent(() => emitter.fire())); observableFromEvent(emitter.event, () => model.getVersionId()).read(r); // position can be wrong diff --git a/src/vs/workbench/contrib/interactive/browser/replInputHintContentWidget.ts b/src/vs/workbench/contrib/interactive/browser/replInputHintContentWidget.ts index 8b17007145f..dec54b24f4b 100644 --- a/src/vs/workbench/contrib/interactive/browser/replInputHintContentWidget.ts +++ b/src/vs/workbench/contrib/interactive/browser/replInputHintContentWidget.ts @@ -12,6 +12,7 @@ import { Disposable } from '../../../../base/common/lifecycle.js'; import { OS } from '../../../../base/common/platform.js'; import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from '../../../../editor/browser/editorBrowser.js'; import { ConfigurationChangedEvent, EditorOption } from '../../../../editor/common/config/editorOptions.js'; +import { Position } from '../../../../editor/common/core/position.js'; import { localize } from '../../../../nls.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; @@ -77,6 +78,8 @@ export class ReplInputHintContentWidget extends Disposable implements IContentWi })); this.editor.applyFontInfo(this.domNode); + const lineHeight = this.editor.getLineHeightForPosition(new Position(1, 1)); + this.domNode.style.lineHeight = lineHeight + 'px'; } return this.domNode; diff --git a/src/vs/workbench/contrib/issue/browser/baseIssueReporterService.ts b/src/vs/workbench/contrib/issue/browser/baseIssueReporterService.ts index 3d7653e56e1..2cecd8c4632 100644 --- a/src/vs/workbench/contrib/issue/browser/baseIssueReporterService.ts +++ b/src/vs/workbench/contrib/issue/browser/baseIssueReporterService.ts @@ -26,7 +26,7 @@ import { IFileDialogService } from '../../../../platform/dialogs/common/dialogs. import { IFileService } from '../../../../platform/files/common/files.js'; import { getIconsStyleSheet } from '../../../../platform/theme/browser/iconsStyleSheet.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; -import { IIssueFormService, IssueReporterData, IssueReporterExtensionData, IssueReporterStyles, IssueType } from '../common/issue.js'; +import { IIssueFormService, IssueReporterData, IssueReporterExtensionData, IssueType } from '../common/issue.js'; import { normalizeGitHubUrl } from '../common/issueReporterUtil.js'; import { IssueReporterModel, IssueReporterData as IssueReporterModelData } from './issueReporterModel.js'; @@ -148,7 +148,6 @@ export class BaseIssueReporterService extends Disposable { this.handleExtensionData(data.enabledExtensions); this.setUpTypes(); - this.applyStyles(data.styles); // Handle case where extension is pre-selected through the command if ((data.data || data.uri) && targetExtension) { @@ -171,85 +170,6 @@ export class BaseIssueReporterService extends Disposable { } } - // TODO @justschen: After migration to Aux Window, switch to dedicated css. - private applyStyles(styles: IssueReporterStyles) { - const styleTag = document.createElement('style'); - const content: string[] = []; - - if (styles.inputBackground) { - content.push(`input[type="text"], textarea, select, .issues-container > .issue > .issue-state, .block-info { background-color: ${styles.inputBackground} !important; }`); - } - - if (styles.backgroundColor) { - content.push(`.monaco-workbench { background-color: ${styles.backgroundColor} !important; }`); - content.push(`.issue-reporter-body::-webkit-scrollbar-track { background-color: ${styles.backgroundColor}; }`); - } - - if (styles.inputBorder) { - content.push(`input[type="text"], textarea, select { border: 1px solid ${styles.inputBorder}; }`); - } else { - content.push(`input[type="text"], textarea, select { border: 1px solid transparent; }`); - } - - if (styles.inputForeground) { - content.push(`input[type="text"], textarea, select, .issues-container > .issue > .issue-state, .block-info { color: ${styles.inputForeground} !important; }`); - } - - if (styles.inputErrorBorder) { - content.push(`.invalid-input, .invalid-input:focus, .validation-error { border: 1px solid ${styles.inputErrorBorder} !important; }`); - content.push(`.required-input { color: ${styles.inputErrorBorder}; }`); - } - - if (styles.inputErrorBackground) { - content.push(`.validation-error { background: ${styles.inputErrorBackground}; }`); - } - - if (styles.inputErrorForeground) { - content.push(`.validation-error { color: ${styles.inputErrorForeground}; }`); - } - - if (styles.inputActiveBorder) { - content.push(`input[type='text']:focus, textarea:focus, select:focus, summary:focus, button:focus, a:focus, .workbenchCommand:focus { border: 1px solid ${styles.inputActiveBorder}; outline-style: none; }`); - } - - if (styles.textLinkColor) { - content.push(`a, .workbenchCommand { color: ${styles.textLinkColor}; }`); - } - - if (styles.textLinkColor) { - content.push(`a { color: ${styles.textLinkColor}; }`); - } - - if (styles.textLinkActiveForeground) { - content.push(`a:hover, .workbenchCommand:hover { color: ${styles.textLinkActiveForeground}; }`); - } - - if (styles.sliderActiveColor) { - content.push(`.issue-reporter-body::-webkit-scrollbar-thumb:active { background-color: ${styles.sliderActiveColor}; }`); - } - - if (styles.sliderHoverColor) { - content.push(`.issue-reporter-body::-webkit-scrollbar-thumb { background-color: ${styles.sliderHoverColor}; }`); - content.push(`.issue-reporter-body::--webkit-scrollbar-thumb:hover { background-color: ${styles.sliderHoverColor}; }`); - } - - if (styles.buttonBackground) { - content.push(`.monaco-text-button { background-color: ${styles.buttonBackground} !important; }`); - } - - if (styles.buttonForeground) { - content.push(`.monaco-text-button { color: ${styles.buttonForeground} !important; }`); - } - - if (styles.buttonHoverBackground) { - content.push(`.monaco-text-button:not(.disabled):hover, .monaco-text-button:focus { background-color: ${styles.buttonHoverBackground} !important; }`); - } - - styleTag.textContent = content.join('\n'); - this.window.document.head.appendChild(styleTag); - this.window.document.body.style.color = styles.color || ''; - } - private async updateIssueReporterUri(extension: IssueReporterExtensionData): Promise { try { if (extension.uri) { @@ -731,21 +651,6 @@ export class BaseIssueReporterService extends Disposable { similarIssues.innerText = ''; if (result && result.items) { this.displaySearchResults(result.items); - } else { - // If the items property isn't present, the rate limit has been hit - const message = $('div.list-title'); - message.textContent = localize('rateLimited', "No duplicate issues found: GitHub query limit exceeded."); - similarIssues.appendChild(message); - - const resetTime = response.headers.get('X-RateLimit-Reset'); - const timeToWait = resetTime ? parseInt(resetTime) - Math.floor(Date.now() / 1000) : 1; - if (this.shouldQueueSearch) { - this.shouldQueueSearch = false; - setTimeout(() => { - this.searchGitHub(repo, title); - this.shouldQueueSearch = true; - }, timeToWait * 1000); - } } }).catch(_ => { console.warn('Timeout or query limit exceeded'); @@ -827,10 +732,6 @@ export class BaseIssueReporterService extends Disposable { similarIssues.appendChild(issuesText); similarIssues.appendChild(issues); - } else { - const message = $('div.list-title'); - message.textContent = localize('noSimilarIssues', "No similar issues found"); - similarIssues.appendChild(message); } } diff --git a/src/vs/workbench/contrib/issue/browser/media/issueReporter.css b/src/vs/workbench/contrib/issue/browser/media/issueReporter.css index 23f3be9ab74..310f1f5220c 100644 --- a/src/vs/workbench/contrib/issue/browser/media/issueReporter.css +++ b/src/vs/workbench/contrib/issue/browser/media/issueReporter.css @@ -17,61 +17,59 @@ * Table */ -.issue-reporter table { +.issue-reporter-body table { width: 100%; max-width: 100%; background-color: transparent; border-collapse: collapse; } -.issue-reporter th { +.issue-reporter-body th { vertical-align: bottom; border-bottom: 1px solid; padding: 5px; text-align: inherit; } -.issue-reporter td { +.issue-reporter-body td { padding: 5px; vertical-align: top; } -.issue-reporter tr td:first-child { +.issue-reporter-body tr td:first-child { width: 30%; } -.issue-reporter label { +.issue-reporter-body label { user-select: none; } -.issue-reporter .block-settingsSearchResults-details { +.issue-reporter-body .block-settingsSearchResults-details { padding-bottom: .5rem; } -.issue-reporter .block-settingsSearchResults-details > div { +.issue-reporter-body .block-settingsSearchResults-details > div { padding: .5rem .75rem; } -.issue-reporter .section { +.issue-reporter-body .section { margin-bottom: .5em; } /** * Forms */ -.issue-reporter input[type="text"], -.issue-reporter textarea { +.issue-reporter-body input[type="text"], +.issue-reporter-body textarea { display: block; width: 100%; padding: .375rem .75rem; font-size: 1rem; line-height: 1.5; color: #495057; - background-color: #fff; - border: 1px solid #ced4da; } -.issue-reporter textarea { +.issue-reporter-body textarea { overflow: auto; resize: vertical; } @@ -80,7 +78,7 @@ * Button */ -.issue-reporter .monaco-text-button { +.issue-reporter-body .monaco-text-button { display: block; width: auto; padding: 4px 10px; @@ -89,113 +87,116 @@ font-size: 13px; } -.issue-reporter select { +.issue-reporter-body select { height: calc(2.25rem + 2px); display: inline-block; padding: 3px 3px; font-size: 14px; line-height: 1.5; color: #495057; - background-color: #fff; + border: none; } -.issue-reporter * { +.issue-reporter-body * { box-sizing: border-box; } -.issue-reporter textarea, -.issue-reporter input, -.issue-reporter select { +.issue-reporter-body .issue-reporter textarea, +.issue-reporter-body .issue-reporter input, +.issue-reporter-body .issue-reporter select { font-family: inherit; } -.issue-reporter html { +.issue-reporter-body html { color: #CCCCCC; height: 100%; } -.issue-reporter .extension-caption .codicon-modifier-spin { +.issue-reporter-body .extension-caption .codicon-modifier-spin { padding-bottom: 3px; margin-left: 2px; } /* Font Families (with CJK support) */ -.issue-reporter .mac.web { +.issue-reporter-body .mac.web { font-family: -apple-system, BlinkMacSystemFont, sans-serif; } -.issue-reporter .mac.web:lang(zh-Hans) { +.issue-reporter-body .mac.web:lang(zh-Hans) { font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Hiragino Sans GB", sans-serif; } -.issue-reporter .mac.web:lang(zh-Hant) { +.issue-reporter-body .mac.web:lang(zh-Hant) { font-family: -apple-system, BlinkMacSystemFont, "PingFang TC", sans-serif; } -.issue-reporter .mac.web:lang(ja) { +.issue-reporter-body .mac.web:lang(ja) { font-family: -apple-system, BlinkMacSystemFont, "Hiragino Kaku Gothic Pro", sans-serif; } -.issue-reporter .mac.web:lang(ko) { +.issue-reporter-body .mac.web:lang(ko) { font-family: -apple-system, BlinkMacSystemFont, "Apple SD Gothic Neo", "Nanum Gothic", "AppleGothic", sans-serif; } -.issue-reporter .windows.web { +.issue-reporter-body .windows.web { font-family: "Segoe WPC", "Segoe UI", sans-serif; } -.issue-reporter .windows.web:lang(zh-Hans) { +.issue-reporter-body .windows.web:lang(zh-Hans) { font-family: "Segoe WPC", "Segoe UI", "Microsoft YaHei", sans-serif; } -.issue-reporter .windows.web:lang(zh-Hant) { +.issue-reporter-body .windows.web:lang(zh-Hant) { font-family: "Segoe WPC", "Segoe UI", "Microsoft Jhenghei", sans-serif; } -.issue-reporter .windows.web:lang(ja) { +.issue-reporter-body .windows.web:lang(ja) { font-family: "Segoe WPC", "Segoe UI", "Yu Gothic UI", "Meiryo UI", sans-serif; } -.issue-reporter .windows.web:lang(ko) { +.issue-reporter-body .windows.web:lang(ko) { font-family: "Segoe WPC", "Segoe UI", "Malgun Gothic", "Dotom", sans-serif; } -.issue-reporter .linux.web { + +/* Linux: add `system-ui` as first font and not `Ubuntu` to allow other distribution pick their standard OS font */ +.issue-reporter-body .linux.web { font-family: system-ui, "Ubuntu", "Droid Sans", sans-serif; } -.issue-reporter .linux.web:lang(zh-Hans) { +.issue-reporter-body .linux.web:lang(zh-Hans) { font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans SC", "Source Han Sans CN", "Source Han Sans", sans-serif; } -.issue-reporter .linux.web:lang(zh-Hant) { +.issue-reporter-body .linux.web:lang(zh-Hant) { font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans TC", "Source Han Sans TW", "Source Han Sans", sans-serif; } -.issue-reporter .linux.web:lang(ja) { +.issue-reporter-body .linux.web:lang(ja) { font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans J", "Source Han Sans JP", "Source Han Sans", sans-serif; } -.issue-reporter .linux.web:lang(ko) { +.issue-reporter-body .linux.web:lang(ko) { font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans K", "Source Han Sans JR", "Source Han Sans", "UnDotum", "FBaekmuk Gulim", sans-serif; } -.issue-reporter body { +body.issue-reporter-body { margin: 0; overflow-y: auto; height: 100%; + background-color: var(--vscode-editor-background) } -.issue-reporter .hidden { +.issue-reporter-body .hidden { display: none; } -.issue-reporter .block { +.issue-reporter-body .block { font-size: 12px; } -.issue-reporter .block .block-info { +.issue-reporter-body .block .block-info { width: 100%; font-size: 12px; overflow: auto; @@ -204,7 +205,7 @@ padding: 10px; } -.issue-reporter { +.issue-reporter-body #issue-reporter { max-width: 85vw; margin-left: auto; margin-right: auto; @@ -215,69 +216,69 @@ overflow: visible; } -.issue-reporter .description-section { +.issue-reporter-body .description-section { flex-grow: 0; display: flex; flex-direction: column; flex-shrink: 0; } -.issue-reporter textarea { +.issue-reporter-body textarea { flex-grow: 1; height: 200px; } -.issue-reporter .block-info-text { +.issue-reporter-body .block-info-text { display: flex; flex-grow: 0; flex-direction: column; } -.issue-reporter #github-submit-btn { +.issue-reporter-body #github-submit-btn { flex-shrink: 0; margin-left: auto; margin-top: 10px; margin-bottom: 10px; } -.issue-reporter .two-col { +.issue-reporter-body .two-col { display: inline-block; width: 49%; } -.issue-reporter #vscode-version { +.issue-reporter-body #vscode-version { width: 90%; } -.issue-reporter .input-group { +.issue-reporter-body .issue-reporter .input-group { margin-bottom: 1em; font-size: 16px; } -.issue-reporter #extension-selection { +.issue-reporter-body #extension-selection { margin-top: 1em; } -.issue-reporter select, -.issue-reporter input, -.issue-reporter textarea { +.issue-reporter-body .issue-reporter select, +.issue-reporter-body .issue-reporter input, +.issue-reporter-body .issue-reporter textarea { border: 1px solid transparent; margin-top: 10px; } -.issue-reporter .validation-error { +.issue-reporter-body .validation-error { font-size: 12px; padding: 10px; border-top: 0px !important; } -.issue-reporter .system-info { +.issue-reporter-body .system-info { margin-bottom: 10px; } -.issue-reporter input[type="checkbox"] { +.issue-reporter-body .issue-reporter input[type="checkbox"] { width: auto; display: inline-block; margin-top: 0; @@ -285,139 +286,155 @@ cursor: pointer; } -.issue-reporter input:disabled { +.issue-reporter-body .issue-reporter input:disabled { opacity: 0.6; } -.issue-reporter .list-title { +.issue-reporter-body .list-title { margin-top: 1em; margin-left: 1em; } -.issue-reporter .instructions { +.issue-reporter-body .instructions { font-size: 12px; margin-top: .5em; } -.issue-reporter a, -.issue-reporter .workbenchCommand { +.issue-reporter-body a, +.issue-reporter-body .workbenchCommand { cursor: pointer; border: 1px solid transparent; + color: var(--vscode-textLink-foreground); } -.issue-reporter .workbenchCommand:disabled { +.issue-reporter-body .workbenchCommand:disabled { color: #868e96; cursor: default } -.issue-reporter .block-extensions .block-info { +.issue-reporter-body .block-extensions .block-info { margin-bottom: 1.5em; } -/* Default styles, overwritten if a theme is provided */ -.issue-reporter input, -.issue-reporter select, -.issue-reporter textarea { - background-color: #3c3c3c; - color: #cccccc; +.issue-reporter-body .showInfo, +.issue-reporter-body .input-group a { + color: var(--vscode-textLink-foreground); } -.issue-reporter .section .input-group .validation-error { +.issue-reporter-body .section .input-group .validation-error { margin-left: 100px; } -.issue-reporter .section .inline-form-control, -.issue-reporter .section .inline-label { +.issue-reporter-body .section .inline-form-control, +.issue-reporter-body .section .inline-label { display: inline-block; + font-size: initial; } -.issue-reporter .section .inline-label { +.issue-reporter-body .section .inline-label { width: 95px; } -.issue-reporter .section .inline-form-control, -.issue-reporter .section .input-group .validation-error { +.issue-reporter-body .section .inline-form-control, +.issue-reporter-body .section .input-group .validation-error { width: calc(100% - 100px); } -.issue-reporter #issue-type { - cursor: pointer; +.issue-reporter-body .issue-reporter .inline-label, +.issue-reporter-body .issue-reporter #issue-description-label { + font-size: initial; + cursor: default; } -.issue-reporter #similar-issues { +.issue-reporter-body .monaco-workbench .issue-reporter label { + cursor: default; +} + +.issue-reporter-body #issue-type, +.issue-reporter-body #issue-source, +.issue-reporter-body #extension-selector { + cursor: pointer; + cursor: pointer; + appearance: auto; + border: none; + border-right: 6px solid transparent; + padding-left: 10px; +} + +.issue-reporter-body #similar-issues { margin-left: 15%; display: block; } -.issue-reporter #problem-source-help-text { +.issue-reporter-body #problem-source-help-text { margin-left: calc(15% + 1em); } @media (max-width: 950px) { - .issue-reporter .section .inline-label { + .issue-reporter-body .section .inline-label { width: 15%; font-size: 16px; } - .issue-reporter #problem-source-help-text { + .issue-reporter-body #problem-source-help-text { margin-left: calc(15% + 1em); } - .issue-reporter .section .inline-form-control, - .issue-reporter .section .input-group .validation-error { + .issue-reporter-body .section .inline-form-control, + .issue-reporter-body .section .input-group .validation-error { width: calc(85% - 5px); } - .issue-reporter .section .input-group .validation-error { + .issue-reporter-body .section .input-group .validation-error { margin-left: calc(15% + 4px); } } @media (max-width: 620px) { - .issue-reporter .section .inline-label { + .issue-reporter-body .section .inline-label { display: none !important; } - .issue-reporter #problem-source-help-text { + .issue-reporter-body #problem-source-help-text { margin-left: 1em; } - .issue-reporter .section .inline-form-control, - .issue-reporter .section .input-group .validation-error { + .issue-reporter-body .section .inline-form-control, + .issue-reporter-body .section .input-group .validation-error { width: 100%; } - .issue-reporter #similar-issues, - .issue-reporter .section .input-group .validation-error { + .issue-reporter-body #similar-issues, + .issue-reporter-body .section .input-group .validation-error { margin-left: 0; } } -.issue-reporter ::-webkit-scrollbar { +.issue-reporter-body::-webkit-scrollbar { width: 14px; } -.issue-reporter ::-webkit-scrollbar-thumb { +.issue-reporter-body::-webkit-scrollbar-thumb { min-height: 20px; } -.issue-reporter ::-webkit-scrollbar-corner { +.issue-reporter-body::-webkit-scrollbar-corner { display: none; } -.issue-reporter .issues-container { +.issue-reporter-body .issues-container { margin-left: 1.5em; margin-top: .5em; max-height: 92px; overflow-y: auto; } -.issue-reporter .issues-container > .issue { +.issue-reporter-body .issues-container > .issue { padding: 4px 0; display: flex; } -.issue-reporter .issues-container > .issue > .issue-link { +.issue-reporter-body .issues-container > .issue > .issue-link { width: calc(100% - 82px); overflow: hidden; padding-top: 3px; @@ -425,11 +442,11 @@ text-overflow: ellipsis; } -.issue-reporter .issues-container > .issue > .issue-state .codicon { +.issue-reporter-body .issues-container > .issue > .issue-state .codicon { width: 16px; } -.issue-reporter .issues-container > .issue > .issue-state { +.issue-reporter-body .issues-container > .issue > .issue-state { display: flex; width: 77px; padding: 3px 6px; @@ -439,7 +456,7 @@ border-radius: .25rem; } -.issue-reporter .issues-container > .issue .label { +.issue-reporter-body .issues-container > .issue .label { padding-top: 2px; margin-left: 5px; width: 44px; @@ -447,11 +464,87 @@ overflow: hidden; } -.issue-reporter .issues-container > .issue .issue-icon { +.issue-reporter-body .issues-container > .issue .issue-icon { padding-top: 2px; } -.issue-reporter a { +.issue-reporter-body a { color: var(--vscode-textLink-foreground); } +.issue-reporter-body .issue-reporter input[type="text"], +.issue-reporter-body .issue-reporter textarea, +.issue-reporter-body .issue-reporter select, +.issue-reporter-body .issue-reporter .issues-container > .issue > .issue-state, +.issue-reporter-body .issue-reporter .block-info { + background-color: var(--vscode-input-background); + color: var(--vscode-input-foreground); +} + +.issue-reporter-body .monaco-workbench, +.issue-reporter-body::-webkit-scrollbar-track { + background-color: var(--vscode-editor-background) !important; +} + +.issue-reporter-body .issue-reporter input[type="text"], +.issue-reporter-body .issue-reporter textarea, +.issue-reporter-body .issue-reporter select { + border: 1px solid var(--vscode-input-border) +} + +.issue-reporter-body .issue-reporter input[type='text']:focus, +.issue-reporter-body .issue-reporter textarea:focus, +.issue-reporter-body .issue-reporter select:focus, +.issue-reporter-body .issue-reporter summary:focus, +.issue-reporter-body .issue-reporter button:focus, +.issue-reporter-body .issue-reporter a:focus, +.issue-reporter-body .issue-reporter .workbenchCommand:focus { + border: 1px solid var(--vscode-inputOption-activeBorder); + outline-style: none; +} + +.issue-reporter-body .invalid-input, +.issue-reporter-body .invalid-input:focus, +.issue-reporter-body .validation-error { + border: 1px solid var(--vscode-inputValidation-errorBorder) !important +} + +.issue-reporter-body .required-input { + color: var(--vscode-inputValidation-errorBorder) +} + +.issue-reporter-body .validation-error { + background: var(--vscode-inputValidation-errorBackground); + color: var(--vscode-inputValidation-errorForeground) +} + +.issue-reporter-body a, +.issue-reporter-body .workbenchCommand { + color: var(--vscode-textLink-foreground) +} + +.issue-reporter-body a:hover, +.issue-reporter-body .workbenchCommand:hover { + color: var(--vscode-textLink-activeForeground) +} + +.issue-reporter-body::-webkit-scrollbar-thumb:active { + background-color: var(--vscode-scrollbarSlider-activeBackground) +} + + +.issue-reporter-body::-webkit-scrollbar-thumb, +.issue-reporter-body::-webkit-scrollbar-thumb:hover { + background-color: var(--vscode-scrollbarSlider-hoverBackground) +} + +.issue-reporter-update-banner { + color: var(--vscode-textLink-foreground); + color: var(--vscode-button-foreground); + background-color: var(--vscode-button-background); + padding: 10px; + text-align: center; + position: sticky; + top: 0; + z-index: 1000; +} diff --git a/src/vs/workbench/contrib/issue/electron-sandbox/media/issueReporter.css b/src/vs/workbench/contrib/issue/electron-sandbox/media/issueReporter.css deleted file mode 100644 index aebbe980428..00000000000 --- a/src/vs/workbench/contrib/issue/electron-sandbox/media/issueReporter.css +++ /dev/null @@ -1,480 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -/** - * Table - */ - -.issue-reporter-body table { - width: 100%; - max-width: 100%; - background-color: transparent; - border-collapse: collapse; -} - -.issue-reporter-body th { - vertical-align: bottom; - border-bottom: 1px solid; - padding: 5px; - text-align: inherit; -} - -.issue-reporter-body td { - padding: 5px; - vertical-align: top; -} - -.issue-reporter-body tr td:first-child { - width: 30%; -} - -.issue-reporter-body label { - user-select: none; -} - -.issue-reporter-body .block-settingsSearchResults-details { - padding-bottom: .5rem; -} - -.issue-reporter-body .block-settingsSearchResults-details > div { - padding: .5rem .75rem; -} - -.issue-reporter-body .section { - margin-bottom: .5em; -} - -/** - * Forms - */ -.issue-reporter-body input[type="text"], -.issue-reporter-body textarea { - display: block; - width: 100%; - padding: .375rem .75rem; - font-size: 1rem; - line-height: 1.5; - color: #495057; - background-color: #fff; - border: 1px solid #ced4da; -} - -.issue-reporter-body textarea { - overflow: auto; - resize: vertical; -} - -/** - * Button - */ - -.issue-reporter-body .monaco-text-button { - display: block; - width: auto; - padding: 4px 10px; - align-self: flex-end; - margin-bottom: 1em; - font-size: 13px; -} - -.issue-reporter-body select { - height: calc(2.25rem + 2px); - display: inline-block; - padding: 3px 3px; - font-size: 14px; - line-height: 1.5; - color: #495057; - background-color: #fff; - border: none; -} - -.issue-reporter-body * { - box-sizing: border-box; -} - -.issue-reporter-body textarea, -.issue-reporter-body input, -.issue-reporter-body select { - font-family: inherit; -} - -.issue-reporter-body html { - color: #CCCCCC; - height: 100%; -} - -.issue-reporter-body .extension-caption .codicon-modifier-spin { - padding-bottom: 3px; - margin-left: 2px; -} - -/* Font Families (with CJK support) */ - -.issue-reporter-body .mac { - font-family: -apple-system, BlinkMacSystemFont, sans-serif; -} - -.issue-reporter-body .mac:lang(zh-Hans) { - font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Hiragino Sans GB", sans-serif; -} - -.issue-reporter-body .mac:lang(zh-Hant) { - font-family: -apple-system, BlinkMacSystemFont, "PingFang TC", sans-serif; -} - -.issue-reporter-body .mac:lang(ja) { - font-family: -apple-system, BlinkMacSystemFont, "Hiragino Kaku Gothic Pro", sans-serif; -} - -.issue-reporter-body .mac:lang(ko) { - font-family: -apple-system, BlinkMacSystemFont, "Apple SD Gothic Neo", "Nanum Gothic", "AppleGothic", sans-serif; -} - -.issue-reporter-body .windows { - font-family: "Segoe WPC", "Segoe UI", sans-serif; -} - -.issue-reporter-body .windows:lang(zh-Hans) { - font-family: "Segoe WPC", "Segoe UI", "Microsoft YaHei", sans-serif; -} - -.issue-reporter-body .windows:lang(zh-Hant) { - font-family: "Segoe WPC", "Segoe UI", "Microsoft Jhenghei", sans-serif; -} - -.issue-reporter-body .windows:lang(ja) { - font-family: "Segoe WPC", "Segoe UI", "Yu Gothic UI", "Meiryo UI", sans-serif; -} - -.issue-reporter-body .windows:lang(ko) { - font-family: "Segoe WPC", "Segoe UI", "Malgun Gothic", "Dotom", sans-serif; -} - -/* Linux: add `system-ui` as first font and not `Ubuntu` to allow other distribution pick their standard OS font */ -.issue-reporter-body .linux { - font-family: system-ui, "Ubuntu", "Droid Sans", sans-serif; -} - -.issue-reporter-body .linux:lang(zh-Hans) { - font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans SC", "Source Han Sans CN", "Source Han Sans", sans-serif; -} - -.issue-reporter-body .linux:lang(zh-Hant) { - font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans TC", "Source Han Sans TW", "Source Han Sans", sans-serif; -} - -.issue-reporter-body .linux:lang(ja) { - font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans J", "Source Han Sans JP", "Source Han Sans", sans-serif; -} - -.issue-reporter-body .linux:lang(ko) { - font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans K", "Source Han Sans JR", "Source Han Sans", "UnDotum", "FBaekmuk Gulim", sans-serif; -} - -body.issue-reporter-body { - margin: 0 !important; - overflow-y: auto !important; - height: 100% !important; -} - -.issue-reporter-body .hidden { - display: none; -} - -.issue-reporter-body .block { - font-size: 12px; -} - -.issue-reporter-body .block .block-info { - width: 100%; - font-size: 12px; - overflow: auto; - overflow-wrap: break-word; - margin: 5px; - padding: 10px; -} - -.issue-reporter-body #issue-reporter { - max-width: 85vw; - margin-left: auto; - margin-right: auto; - padding-top: 2em; - padding-bottom: 2em; - display: flex; - flex-direction: column; - overflow: visible; -} - -.issue-reporter-body .description-section { - flex-grow: 0; - display: flex; - flex-direction: column; - flex-shrink: 0; -} - -.issue-reporter-body textarea { - flex-grow: 0; - height: 200px; -} - -.issue-reporter-body .block-info-text { - display: flex; - flex-grow: 0; - flex-direction: column; -} - -.issue-reporter-body #github-submit-btn { - flex-shrink: 0; - margin-left: auto; - margin-top: 10px; - margin-bottom: 10px; -} - -.issue-reporter-body .two-col { - display: inline-block; - width: 49%; -} - -.issue-reporter-body #vscode-version { - width: 90%; -} - -.issue-reporter-body .input-group { - margin-bottom: 1em; -} - -.issue-reporter-body #extension-selection { - margin-top: 1em; -} - -.issue-reporter-body .issue-reporter select, -.issue-reporter-body .issue-reporter input, -.issue-reporter-body .issue-reporter textarea { - border: 1px solid transparent; - margin-top: 10px; -} - - -.issue-reporter-body #issue-reporter .validation-error { - font-size: 12px; - padding: 10px; - border-top: 0px !important; -} - -.issue-reporter-body #issue-reporter .system-info { - margin-bottom: 10px; -} - - -.issue-reporter-body input[type="checkbox"] { - width: auto; - display: inline-block; - margin-top: 0; - vertical-align: middle; - cursor: pointer; -} - -.issue-reporter-body input:disabled { - opacity: 0.6; -} - -.issue-reporter-body .list-title { - margin-top: 1em; - margin-left: 1em; -} - -.issue-reporter-body .instructions { - font-size: 12px; - margin-top: .5em; -} - -.issue-reporter-body a, -.issue-reporter-body .workbenchCommand { - cursor: pointer; - border: 1px solid transparent; -} - -.issue-reporter-body .workbenchCommand:disabled { - color: #868e96; - cursor: default -} - -.issue-reporter-body .block-extensions .block-info { - margin-bottom: 1.5em; -} - -/* Default styles, overwritten if a theme is provided */ -.issue-reporter-body input, -.issue-reporter-body select, -.issue-reporter-body textarea { - background-color: #3c3c3c; - border: none; - color: #cccccc; -} - -.issue-reporter-body a { - color: #CCCCCC; - text-decoration: none; -} - -.issue-reporter-body .showInfo, -.issue-reporter-body .input-group a { - color: var(--vscode-textLink-foreground); -} - -.issue-reporter-body .section .input-group .validation-error { - margin-left: 100px; -} - -.issue-reporter-body .section .inline-form-control, -.issue-reporter-body .section .inline-label { - display: inline-block; - font-size: initial; -} - -.issue-reporter-body .section .inline-label { - width: 95px; -} - -.issue-reporter-body .issue-reporter .inline-label, -.issue-reporter-body .issue-reporter #issue-description-label { - font-size: initial; - cursor: default; -} - -.issue-reporter-body .monaco-workbench .issue-reporter label { - cursor: default; -} - -.issue-reporter-body .section .inline-form-control, -.issue-reporter-body .section .input-group .validation-error { - width: calc(100% - 100px); -} - -.issue-reporter-body #issue-type, -.issue-reporter-body #issue-source, -.issue-reporter-body #extension-selector { - cursor: pointer; - appearance: auto; - border: none; - border-right: 6px solid transparent; - padding-left: 10px; -} - -.issue-reporter-body #similar-issues { - margin-left: 15%; - display: block; -} - -.issue-reporter-body #problem-source-help-text { - margin-left: calc(15% + 1em); -} - -@media (max-width: 950px) { - .issue-reporter-body .section .inline-label { - width: 15%; - } - - .issue-reporter-body #problem-source-help-text { - margin-left: calc(15% + 1em); - } - - .issue-reporter-body .section .inline-form-control, - .issue-reporter-body .section .input-group .validation-error { - width: calc(85% - 5px); - } - - .issue-reporter-body .section .input-group .validation-error { - margin-left: calc(15% + 4px); - } -} - -@media (max-width: 620px) { - .issue-reporter-body .section .inline-label { - display: none !important; - } - - .issue-reporter-body #problem-source-help-text { - margin-left: 1em; - } - - .issue-reporter-body .section .inline-form-control, - .issue-reporter-body .section .input-group .validation-error { - width: 100%; - } - - .issue-reporter-body #similar-issues, - .issue-reporter-body .section .input-group .validation-error { - margin-left: 0; - } -} - -.issue-reporter-body::-webkit-scrollbar { - width: 14px; -} - -.issue-reporter-body::-webkit-scrollbar-thumb { - min-height: 20px; -} - -.issue-reporter-body::-webkit-scrollbar-corner { - display: none; -} - -.issue-reporter-body .issues-container { - margin-left: 1.5em; - margin-top: .5em; - max-height: 92px; - overflow-y: auto; -} - -.issue-reporter-body .issues-container > .issue { - padding: 4px 0; - display: flex; -} - -.issue-reporter-body .issues-container > .issue > .issue-link { - width: calc(100% - 82px); - overflow: hidden; - padding-top: 3px; - white-space: nowrap; - text-overflow: ellipsis; -} - -.issue-reporter-body .issues-container > .issue > .issue-state .codicon { - width: 16px; -} - -.issue-reporter-body .issues-container > .issue > .issue-state { - display: flex; - width: 77px; - padding: 3px 6px; - margin-right: 5px; - color: #CCCCCC; - background-color: #3c3c3c; - border-radius: .25rem; -} - -.issue-reporter-body .issues-container > .issue .label { - padding-top: 2px; - margin-left: 5px; - width: 44px; - text-overflow: ellipsis; - overflow: hidden; -} - -.issue-reporter-body .issues-container > .issue .issue-icon { - padding-top: 2px; -} - -.issue-reporter-update-banner { - color: var(--vscode-button-foreground); - background-color: var(--vscode-button-background); - padding: 10px; - text-align: center; - position: sticky; - top: 0; - z-index: 1000; -} diff --git a/src/vs/workbench/contrib/issue/electron-sandbox/nativeIssueFormService.ts b/src/vs/workbench/contrib/issue/electron-sandbox/nativeIssueFormService.ts index f6ae15199b8..831ec00e7c4 100644 --- a/src/vs/workbench/contrib/issue/electron-sandbox/nativeIssueFormService.ts +++ b/src/vs/workbench/contrib/issue/electron-sandbox/nativeIssueFormService.ts @@ -17,7 +17,6 @@ import { IHostService } from '../../../services/host/browser/host.js'; import { IssueFormService } from '../browser/issueFormService.js'; import { IIssueFormService, IssueReporterData } from '../common/issue.js'; import { IssueReporter } from './issueReporterService.js'; -import './media/issueReporter.css'; export class NativeIssueFormService extends IssueFormService implements IIssueFormService { private readonly store = new DisposableStore(); diff --git a/src/vs/workbench/contrib/markers/browser/markers.contribution.ts b/src/vs/workbench/contrib/markers/browser/markers.contribution.ts index c7fb9f38e4f..b22f84b628a 100644 --- a/src/vs/workbench/contrib/markers/browser/markers.contribution.ts +++ b/src/vs/workbench/contrib/markers/browser/markers.contribution.ts @@ -16,7 +16,7 @@ import { MenuId, registerAction2, Action2 } from '../../../../platform/actions/c import { Registry } from '../../../../platform/registry/common/platform.js'; import { MarkersViewMode, Markers, MarkersContextKeys } from '../common/markers.js'; import Messages from './messages.js'; -import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from '../../../common/contributions.js'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; import { IMarkersView } from './markers.js'; import { LifecyclePhase } from '../../../services/lifecycle/common/lifecycle.js'; import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js'; @@ -36,6 +36,7 @@ import { IActivityService, NumberBadge } from '../../../services/activity/common import { viewFilterSubmenu } from '../../../browser/parts/views/viewFilter.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { problemsConfigurationNodeBase } from '../../../common/configuration.js'; +import { MarkerChatContextContribution } from './markersChatContext.js'; KeybindingsRegistry.registerCommandAndKeybindingRule({ id: Markers.MARKER_OPEN_ACTION_ID, @@ -686,6 +687,8 @@ class MarkersStatusBarContributions extends Disposable implements IWorkbenchCont workbenchRegistry.registerWorkbenchContribution(MarkersStatusBarContributions, LifecyclePhase.Restored); +registerWorkbenchContribution2(MarkerChatContextContribution.ID, MarkerChatContextContribution, WorkbenchPhase.AfterRestored); + class ActivityUpdater extends Disposable implements IWorkbenchContribution { private readonly activity = this._register(new MutableDisposable()); diff --git a/src/vs/workbench/contrib/markers/browser/markersChatContext.ts b/src/vs/workbench/contrib/markers/browser/markersChatContext.ts new file mode 100644 index 00000000000..83bff8daa8c --- /dev/null +++ b/src/vs/workbench/contrib/markers/browser/markersChatContext.ts @@ -0,0 +1,88 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + + +import { groupBy } from '../../../../base/common/arrays.js'; +import { Codicon } from '../../../../base/common/codicons.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { extUri } from '../../../../base/common/resources.js'; +import { localize } from '../../../../nls.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { ILabelService } from '../../../../platform/label/common/label.js'; +import { IMarkerService, MarkerSeverity } from '../../../../platform/markers/common/markers.js'; +import { IQuickPickSeparator } from '../../../../platform/quickinput/common/quickInput.js'; +import { IWorkbenchContribution } from '../../../common/contributions.js'; +import { IChatContextPickerItem, IChatContextPickerPickItem, IChatContextPickService } from '../../chat/browser/chatContextPickService.js'; +import { IDiagnosticVariableEntryFilterData } from '../../chat/common/chatModel.js'; + +class MarkerChatContextPick implements IChatContextPickerItem { + + readonly type = 'pickerPick'; + readonly label = localize('chatContext.diagnstic', 'Problems...'); + readonly icon = Codicon.error; + readonly ordinal = -100; + + constructor( + @IMarkerService private readonly _markerService: IMarkerService, + @ILabelService private readonly _labelService: ILabelService, + ) { } + + asPicker(): { readonly placeholder: string; readonly picks: Promise<(IChatContextPickerPickItem | IQuickPickSeparator)[]> } { + + const markers = this._markerService.read({ severities: MarkerSeverity.Error | MarkerSeverity.Warning | MarkerSeverity.Info }); + const grouped = groupBy(markers, (a, b) => extUri.compare(a.resource, b.resource)); + + const severities = new Set(); + const items: (IChatContextPickerPickItem | IQuickPickSeparator)[] = []; + + let pickCount = 0; + for (const group of grouped) { + const resource = group[0].resource; + + items.push({ type: 'separator', label: this._labelService.getUriLabel(resource, { relative: true }) }); + for (const marker of group) { + pickCount++; + severities.add(marker.severity); + + items.push({ + label: marker.message, + description: localize('markers.panel.at.ln.col.number', "[Ln {0}, Col {1}]", '' + marker.startLineNumber, '' + marker.startColumn), + asAttachment() { + return IDiagnosticVariableEntryFilterData.toEntry(IDiagnosticVariableEntryFilterData.fromMarker(marker)); + } + }); + } + } + + items.unshift({ + label: localize('markers.panel.allErrors', 'All Problems'), + asAttachment() { + return IDiagnosticVariableEntryFilterData.toEntry({ + filterSeverity: MarkerSeverity.Info + }); + }, + }); + + + return { + placeholder: localize('chatContext.diagnstic.placeholder', 'Select a problem to attach'), + picks: Promise.resolve(items) + }; + } +} + + +export class MarkerChatContextContribution extends Disposable implements IWorkbenchContribution { + + static readonly ID = 'workbench.contrib.chat.markerChatContextContribution'; + + constructor( + @IChatContextPickService contextPickService: IChatContextPickService, + @IInstantiationService instantiationService: IInstantiationService, + ) { + super(); + this._store.add(contextPickService.registerChatContextItem(instantiationService.createInstance(MarkerChatContextPick))); + } +} diff --git a/src/vs/workbench/contrib/mcp/browser/mcp.contribution.ts b/src/vs/workbench/contrib/mcp/browser/mcp.contribution.ts index b4b3aaed20f..6e7980efeca 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcp.contribution.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcp.contribution.ts @@ -3,13 +3,20 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { localize, localize2 } from '../../../../nls.js'; import { registerAction2 } from '../../../../platform/actions/common/actions.js'; +import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import * as jsonContributionRegistry from '../../../../platform/jsonschemas/common/jsonContributionRegistry.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; +import { EditorPaneDescriptor, IEditorPaneRegistry } from '../../../browser/editor.js'; import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; +import { EditorExtensions } from '../../../common/editor.js'; +import { IViewsRegistry, Extensions as ViewExtensions } from '../../../common/views.js'; import { mcpSchemaId } from '../../../services/configuration/common/configuration.js'; +import { VIEW_CONTAINER } from '../../extensions/browser/extensions.contribution.js'; +import { DefaultViewsContext, SearchMcpServersContext } from '../../extensions/common/extensions.js'; import { ConfigMcpDiscovery } from '../common/discovery/configMcpDiscovery.js'; import { ExtensionMcpDiscovery } from '../common/discovery/extensionMcpDiscovery.js'; import { mcpDiscoveryRegistry } from '../common/discovery/mcpDiscovery.js'; @@ -21,14 +28,19 @@ import { McpContextKeysController } from '../common/mcpContextKeys.js'; import { McpRegistry } from '../common/mcpRegistry.js'; import { IMcpRegistry } from '../common/mcpRegistryTypes.js'; import { McpService } from '../common/mcpService.js'; -import { IMcpService } from '../common/mcpTypes.js'; -import { AddConfigurationAction, EditStoredInput, InstallFromActivation, ListMcpServerCommand, MCPServerActionRendering, McpServerOptionsCommand, RemoveStoredInput, ResetMcpCachedTools, ResetMcpTrustCommand, RestartServer, ShowConfiguration, ShowOutput, StartServer, StopServer } from './mcpCommands.js'; +import { HasInstalledMcpServersContext, IMcpService, IMcpWorkbenchService, InstalledMcpServersViewId, McpServersGalleryEnabledContext } from '../common/mcpTypes.js'; +import { AddConfigurationAction, EditStoredInput, InstallFromActivation, ListMcpServerCommand, McpBrowseCommand, MCPServerActionRendering, McpServerOptionsCommand, RemoveStoredInput, ResetMcpCachedTools, ResetMcpTrustCommand, RestartServer, ShowConfiguration, ShowOutput, StartServer, StopServer } from './mcpCommands.js'; import { McpDiscovery } from './mcpDiscovery.js'; import { McpLanguageFeatures } from './mcpLanguageFeatures.js'; +import { McpServerEditor } from './mcpServerEditor.js'; +import { McpServerEditorInput } from './mcpServerEditorInput.js'; +import { McpServersListView } from './mcpServersView.js'; import { McpUrlHandler } from './mcpUrlHandler.js'; +import { MCPContextsInitialisation, McpWorkbenchService } from './mcpWorkbenchService.js'; registerSingleton(IMcpRegistry, McpRegistry, InstantiationType.Delayed); registerSingleton(IMcpService, McpService, InstantiationType.Delayed); +registerSingleton(IMcpWorkbenchService, McpWorkbenchService, InstantiationType.Eager); registerSingleton(IMcpConfigPathsService, McpConfigPathsService, InstantiationType.Delayed); mcpDiscoveryRegistry.register(new SyncDescriptor(RemoteNativeMpcDiscovery)); @@ -54,8 +66,38 @@ registerAction2(ShowOutput); registerAction2(InstallFromActivation); registerAction2(RestartServer); registerAction2(ShowConfiguration); +registerAction2(McpBrowseCommand); registerWorkbenchContribution2('mcpActionRendering', MCPServerActionRendering, WorkbenchPhase.BlockRestore); +registerWorkbenchContribution2(MCPContextsInitialisation.ID, MCPContextsInitialisation, WorkbenchPhase.AfterRestored); const jsonRegistry = Registry.as(jsonContributionRegistry.Extensions.JSONContribution); jsonRegistry.registerSchema(mcpSchemaId, mcpServerSchema); + +Registry.as(ViewExtensions.ViewsRegistry).registerViews([ + { + id: InstalledMcpServersViewId, + name: localize2('mcp-installed', "MCP Servers - Installed"), + ctorDescriptor: new SyncDescriptor(McpServersListView), + when: ContextKeyExpr.and(DefaultViewsContext, HasInstalledMcpServersContext, McpServersGalleryEnabledContext), + weight: 40, + order: 4, + canToggleVisibility: true + }, + { + id: 'workbench.views.mcp.marketplace', + name: localize2('mcp', "MCP Servers"), + ctorDescriptor: new SyncDescriptor(McpServersListView), + when: ContextKeyExpr.and(SearchMcpServersContext, McpServersGalleryEnabledContext), + } +], VIEW_CONTAINER); + +Registry.as(EditorExtensions.EditorPane).registerEditorPane( + EditorPaneDescriptor.create( + McpServerEditor, + McpServerEditor.ID, + localize('mcpServer', "MCP Server") + ), + [ + new SyncDescriptor(McpServerEditorInput) + ]); diff --git a/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts b/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts index 6166392e158..362cd3c822e 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts @@ -32,10 +32,14 @@ import { ChatMode } from '../../chat/common/constants.js'; import { TEXT_FILE_EDITOR_ID } from '../../files/common/files.js'; import { McpContextKeys } from '../common/mcpContextKeys.js'; import { IMcpRegistry } from '../common/mcpRegistryTypes.js'; -import { IMcpServer, IMcpService, LazyCollectionState, McpConnectionState, McpServerToolsState } from '../common/mcpTypes.js'; +import { IMcpServer, IMcpService, IMcpWorkbenchService, InstalledMcpServersViewId, LazyCollectionState, McpConnectionState, McpServersGalleryEnabledContext, McpServerToolsState } from '../common/mcpTypes.js'; import { McpAddConfigurationCommand } from './mcpCommandsAddConfiguration.js'; import { McpUrlHandler } from './mcpUrlHandler.js'; import { McpCommandIds } from '../common/mcpCommandIds.js'; +import { extensionsFilterSubMenu, IExtensionsWorkbenchService } from '../../extensions/common/extensions.js'; +import { ExtensionsLocalizedLabel } from '../../../../platform/extensionManagement/common/extensionManagement.js'; +import { IMcpGalleryService } from '../../../../platform/mcp/common/mcpManagement.js'; +import { IViewsService } from '../../../services/views/common/viewsService.js'; // acroynms do not get localized const category: ILocalizedString = { @@ -56,9 +60,9 @@ export class ListMcpServerCommand extends Action2 { ContextKeyExpr.or(McpContextKeys.hasUnknownTools, McpContextKeys.hasServersWithErrors), ChatContextKeys.chatMode.isEqualTo(ChatMode.Agent) ), - id: MenuId.ChatInput, + id: MenuId.ChatExecute, group: 'navigation', - order: 101 + order: 2, }, }); } @@ -67,6 +71,18 @@ export class ListMcpServerCommand extends Action2 { const mcpService = accessor.get(IMcpService); const commandService = accessor.get(ICommandService); const quickInput = accessor.get(IQuickInputService); + const mcpWorkbenchService = accessor.get(IMcpWorkbenchService); + const extensionWorkbenchService = accessor.get(IExtensionsWorkbenchService); + const viewsService = accessor.get(IViewsService); + const mcpGalleryService = accessor.get(IMcpGalleryService); + + if (mcpGalleryService.isEnabled()) { + if (mcpWorkbenchService.local.length) { + return viewsService.openView(InstalledMcpServersViewId, true); + } else { + return extensionWorkbenchService.openSearch('@mcp'); + } + } type ItemType = { id: string } & IQuickPickItem; @@ -566,3 +582,26 @@ export class InstallFromActivation extends Action2 { addConfigHelper.pickForUrlHandler(uri); } } + +export class McpBrowseCommand extends Action2 { + constructor() { + super({ + id: McpCommandIds.Browse, + title: localize2('mcp.command.browse', "MCP Servers"), + category: ExtensionsLocalizedLabel, + menu: [{ + id: MenuId.CommandPalette, + when: McpServersGalleryEnabledContext, + }, { + id: extensionsFilterSubMenu, + when: McpServersGalleryEnabledContext, + group: '1_predefined', + order: 1, + }], + }); + } + + async run(accessor: ServicesAccessor) { + accessor.get(IExtensionsWorkbenchService).openSearch('@mcp '); + } +} diff --git a/src/vs/workbench/contrib/mcp/browser/mcpCommandsAddConfiguration.ts b/src/vs/workbench/contrib/mcp/browser/mcpCommandsAddConfiguration.ts index 328158592db..2344c6aac0c 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpCommandsAddConfiguration.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpCommandsAddConfiguration.ts @@ -16,6 +16,7 @@ import { localize } from '../../../../nls.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { ConfigurationTarget, getConfigValueInTarget, IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IFileService } from '../../../../platform/files/common/files.js'; +import { IMcpGalleryService } from '../../../../platform/mcp/common/mcpManagement.js'; import { IMcpConfiguration, IMcpConfigurationHTTP, McpConfigurationServer } from '../../../../platform/mcp/common/mcpPlatformTypes.js'; import { INotificationService } from '../../../../platform/notification/common/notification.js'; import { IQuickInputService, IQuickPickItem, QuickPickInput } from '../../../../platform/quickinput/common/quickInput.js'; @@ -112,10 +113,11 @@ export class McpAddConfigurationCommand { @INotificationService private readonly _notificationService: INotificationService, @ITelemetryService private readonly _telemetryService: ITelemetryService, @IMcpService private readonly _mcpService: IMcpService, + @IMcpGalleryService private readonly _mcpGalleryService: IMcpGalleryService, ) { } private async getServerType(): Promise { - const items: QuickPickInput<{ kind: AddConfigurationType } & IQuickPickItem>[] = [ + const items: QuickPickInput<{ kind: AddConfigurationType | 'browse' } & IQuickPickItem>[] = [ { kind: AddConfigurationType.Stdio, label: localize('mcp.serverType.command', "Command (stdio)"), description: localize('mcp.serverType.command.description', "Run a local command that implements the MCP protocol") }, { kind: AddConfigurationType.HTTP, label: localize('mcp.serverType.http', "HTTP (HTTP or Server-Sent Events)"), description: localize('mcp.serverType.http.description', "Connect to a remote HTTP server that implements the MCP protocol") } ]; @@ -139,10 +141,25 @@ export class McpAddConfigurationCommand { ); } - const result = await this._quickInputService.pick<{ kind: AddConfigurationType } & IQuickPickItem>(items, { + if (this._mcpGalleryService.isEnabled()) { + items.push( + { type: 'separator' }, + { + kind: 'browse', + label: localize('mcp.servers.browse', "Browse MCP Servers..."), + } + ); + } + + const result = await this._quickInputService.pick<{ kind: AddConfigurationType | 'browse' } & IQuickPickItem>(items, { placeHolder: localize('mcp.serverType.placeholder', "Choose the type of MCP server to add"), }); + if (result?.kind === 'browse') { + this._commandService.executeCommand(McpCommandIds.Browse); + return undefined; + } + return result?.kind; } diff --git a/src/vs/workbench/contrib/mcp/browser/mcpServerActions.ts b/src/vs/workbench/contrib/mcp/browser/mcpServerActions.ts new file mode 100644 index 00000000000..b4990844947 --- /dev/null +++ b/src/vs/workbench/contrib/mcp/browser/mcpServerActions.ts @@ -0,0 +1,458 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ActionViewItem, IActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js'; +import { Action, IAction, Separator } from '../../../../base/common/actions.js'; +import { disposeIfDisposable } from '../../../../base/common/lifecycle.js'; +import { ThemeIcon } from '../../../../base/common/themables.js'; +import { localize } from '../../../../nls.js'; +import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { manageExtensionIcon } from '../../extensions/browser/extensionsIcons.js'; +import { getDomNodePagePosition } from '../../../../base/browser/dom.js'; +import { IMcpServer, IMcpServerContainer, IMcpService, IMcpWorkbenchService, IWorkbenchMcpServer, McpConnectionState } from '../common/mcpTypes.js'; +import { IMcpRegistry } from '../common/mcpRegistryTypes.js'; +import { URI } from '../../../../base/common/uri.js'; +import { Location } from '../../../../editor/common/languages.js'; +import { IEditorService } from '../../../services/editor/common/editorService.js'; + +export abstract class McpServerAction extends Action implements IMcpServerContainer { + + static readonly EXTENSION_ACTION_CLASS = 'extension-action'; + static readonly TEXT_ACTION_CLASS = `${McpServerAction.EXTENSION_ACTION_CLASS} text`; + static readonly LABEL_ACTION_CLASS = `${McpServerAction.EXTENSION_ACTION_CLASS} label`; + static readonly PROMINENT_LABEL_ACTION_CLASS = `${McpServerAction.LABEL_ACTION_CLASS} prominent`; + static readonly ICON_ACTION_CLASS = `${McpServerAction.EXTENSION_ACTION_CLASS} icon`; + + private _mcpServer: IWorkbenchMcpServer | null = null; + get mcpServer(): IWorkbenchMcpServer | null { return this._mcpServer; } + set mcpServer(mcpServer: IWorkbenchMcpServer | null) { this._mcpServer = mcpServer; this.update(); } + + abstract update(): void; +} + +export abstract class DropDownAction extends McpServerAction { + + constructor( + id: string, + label: string, + cssClass: string, + enabled: boolean, + @IInstantiationService protected instantiationService: IInstantiationService + ) { + super(id, label, cssClass, enabled); + } + + private _actionViewItem: DropDownExtensionActionViewItem | null = null; + createActionViewItem(options: IActionViewItemOptions): DropDownExtensionActionViewItem { + this._actionViewItem = this.instantiationService.createInstance(DropDownExtensionActionViewItem, this, options); + return this._actionViewItem; + } + + public override run(actionGroups: IAction[][]): Promise { + this._actionViewItem?.showMenu(actionGroups); + return Promise.resolve(); + } +} + +export class DropDownExtensionActionViewItem extends ActionViewItem { + + constructor( + action: IAction, + options: IActionViewItemOptions, + @IContextMenuService private readonly contextMenuService: IContextMenuService + ) { + super(null, action, { ...options, icon: true, label: true }); + } + + public showMenu(menuActionGroups: IAction[][]): void { + if (this.element) { + const actions = this.getActions(menuActionGroups); + const elementPosition = getDomNodePagePosition(this.element); + const anchor = { x: elementPosition.left, y: elementPosition.top + elementPosition.height + 10 }; + this.contextMenuService.showContextMenu({ + getAnchor: () => anchor, + getActions: () => actions, + actionRunner: this.actionRunner, + onHide: () => disposeIfDisposable(actions) + }); + } + } + + private getActions(menuActionGroups: IAction[][]): IAction[] { + let actions: IAction[] = []; + for (const menuActions of menuActionGroups) { + actions = [...actions, ...menuActions, new Separator()]; + } + return actions.length ? actions.slice(0, actions.length - 1) : actions; + } +} + +export class InstallAction extends McpServerAction { + + static readonly CLASS = `${this.LABEL_ACTION_CLASS} prominent install`; + private static readonly HIDE = `${this.CLASS} hide`; + + constructor( + @IMcpWorkbenchService private readonly mcpWorkbenchService: IMcpWorkbenchService, + ) { + super('extensions.install', localize('install', "Install"), InstallAction.CLASS, false); + this.update(); + } + + update(): void { + this.enabled = false; + this.class = InstallAction.HIDE; + if (!this.mcpServer?.gallery) { + return; + } + if (this.mcpServer.local) { + return; + } + this.class = InstallAction.CLASS; + this.enabled = true; + this.label = localize('install', "Install"); + } + + override async run(): Promise { + if (!this.mcpServer) { + return; + } + await this.mcpWorkbenchService.install(this.mcpServer); + } +} + +export class UninstallAction extends McpServerAction { + + static readonly CLASS = `${this.LABEL_ACTION_CLASS} prominent uninstall`; + private static readonly HIDE = `${this.CLASS} hide`; + + constructor( + @IMcpWorkbenchService private readonly mcpWorkbenchService: IMcpWorkbenchService, + ) { + super('extensions.uninstall', localize('uninstall', "Uninstall"), UninstallAction.CLASS, false); + this.update(); + } + + update(): void { + this.enabled = false; + this.class = UninstallAction.HIDE; + if (!this.mcpServer) { + return; + } + if (!this.mcpServer.local) { + return; + } + this.class = UninstallAction.CLASS; + this.enabled = true; + this.label = localize('uninstall', "Uninstall"); + } + + override async run(): Promise { + if (!this.mcpServer) { + return; + } + await this.mcpWorkbenchService.uninstall(this.mcpServer); + } +} + +export class ManageMcpServerAction extends DropDownAction { + + static readonly ID = 'mcpServer.manage'; + + private static readonly Class = `${McpServerAction.ICON_ACTION_CLASS} manage ` + ThemeIcon.asClassName(manageExtensionIcon); + private static readonly HideManageExtensionClass = `${this.Class} hide`; + + constructor( + private readonly isEditorAction: boolean, + @IInstantiationService instantiationService: IInstantiationService, + ) { + + super(ManageMcpServerAction.ID, '', '', true, instantiationService); + this.tooltip = localize('manage', "Manage"); + this.update(); + } + + async getActionGroups(): Promise { + const groups: IAction[][] = []; + groups.push([ + this.instantiationService.createInstance(StartServerAction), + ]); + groups.push([ + this.instantiationService.createInstance(StopServerAction), + this.instantiationService.createInstance(RestartServerAction), + ]); + groups.push([ + this.instantiationService.createInstance(ShowServerOutputAction), + this.instantiationService.createInstance(ShowServerConfigurationAction), + ]); + if (!this.isEditorAction) { + groups.push([ + this.instantiationService.createInstance(UninstallAction), + ]); + } + groups.forEach(group => group.forEach(extensionAction => { + if (extensionAction instanceof McpServerAction) { + extensionAction.mcpServer = this.mcpServer; + } + })); + + return groups; + } + + override async run(): Promise { + return super.run(await this.getActionGroups()); + } + + update(): void { + this.class = ManageMcpServerAction.HideManageExtensionClass; + this.enabled = false; + if (this.mcpServer) { + this.enabled = !!this.mcpServer.local; + this.class = this.enabled ? ManageMcpServerAction.Class : ManageMcpServerAction.HideManageExtensionClass; + } + } +} + +export class StartServerAction extends McpServerAction { + + static readonly CLASS = `${this.LABEL_ACTION_CLASS} prominent start`; + private static readonly HIDE = `${this.CLASS} hide`; + + constructor( + @IMcpService private readonly mcpService: IMcpService, + ) { + super('extensions.start', localize('start', "Start Server"), StartServerAction.CLASS, false); + this.update(); + } + + update(): void { + this.enabled = false; + this.class = StartServerAction.HIDE; + const server = this.getServer(); + if (!server) { + return; + } + const serverState = server.connectionState.get(); + if (!McpConnectionState.canBeStarted(serverState.state)) { + return; + } + this.class = StartServerAction.CLASS; + this.enabled = true; + this.label = localize('start', "Start Server"); + } + + override async run(): Promise { + const server = this.getServer(); + if (!server) { + return; + } + await server.start(true); + server.showOutput(); + } + + private getServer(): IMcpServer | undefined { + if (!this.mcpServer) { + return; + } + if (!this.mcpServer.local) { + return; + } + return this.mcpService.servers.get().find(s => s.definition.label === this.mcpServer?.name); + } +} + +export class StopServerAction extends McpServerAction { + + static readonly CLASS = `${this.LABEL_ACTION_CLASS} prominent stop`; + private static readonly HIDE = `${this.CLASS} hide`; + + constructor( + @IMcpService private readonly mcpService: IMcpService, + ) { + super('extensions.stop', localize('stop', "Stop Server"), StopServerAction.CLASS, false); + this.update(); + } + + update(): void { + this.enabled = false; + this.class = StopServerAction.HIDE; + const server = this.getServer(); + if (!server) { + return; + } + const serverState = server.connectionState.get(); + if (McpConnectionState.canBeStarted(serverState.state)) { + return; + } + this.class = StopServerAction.CLASS; + this.enabled = true; + this.label = localize('stop', "Stop Server"); + } + + override async run(): Promise { + const server = this.getServer(); + if (!server) { + return; + } + await server.stop(); + } + + private getServer(): IMcpServer | undefined { + if (!this.mcpServer) { + return; + } + if (!this.mcpServer.local) { + return; + } + return this.mcpService.servers.get().find(s => s.definition.label === this.mcpServer?.name); + } +} + +export class RestartServerAction extends McpServerAction { + + static readonly CLASS = `${this.LABEL_ACTION_CLASS} prominent restart`; + private static readonly HIDE = `${this.CLASS} hide`; + + constructor( + @IMcpService private readonly mcpService: IMcpService, + ) { + super('extensions.restart', localize('restart', "Restart Server"), RestartServerAction.CLASS, false); + this.update(); + } + + update(): void { + this.enabled = false; + this.class = RestartServerAction.HIDE; + const server = this.getServer(); + if (!server) { + return; + } + const serverState = server.connectionState.get(); + if (McpConnectionState.canBeStarted(serverState.state)) { + return; + } + this.class = RestartServerAction.CLASS; + this.enabled = true; + this.label = localize('restart', "Restart Server"); + } + + override async run(): Promise { + const server = this.getServer(); + if (!server) { + return; + } + await server.stop(); + await server.start(true); + server.showOutput(); + } + + private getServer(): IMcpServer | undefined { + if (!this.mcpServer) { + return; + } + if (!this.mcpServer.local) { + return; + } + return this.mcpService.servers.get().find(s => s.definition.label === this.mcpServer?.name); + } +} + +export class ShowServerOutputAction extends McpServerAction { + + static readonly CLASS = `${this.LABEL_ACTION_CLASS} prominent output`; + private static readonly HIDE = `${this.CLASS} hide`; + + constructor( + @IMcpService private readonly mcpService: IMcpService, + ) { + super('extensions.output', localize('output', "Show Output"), ShowServerOutputAction.CLASS, false); + this.update(); + } + + update(): void { + this.enabled = false; + this.class = ShowServerOutputAction.HIDE; + const server = this.getServer(); + if (!server) { + return; + } + this.class = ShowServerOutputAction.CLASS; + this.enabled = true; + this.label = localize('output', "Show Output"); + } + + override async run(): Promise { + const server = this.getServer(); + if (!server) { + return; + } + server.showOutput(); + } + + private getServer(): IMcpServer | undefined { + if (!this.mcpServer) { + return; + } + if (!this.mcpServer.local) { + return; + } + return this.mcpService.servers.get().find(s => s.definition.label === this.mcpServer?.name); + } +} + +export class ShowServerConfigurationAction extends McpServerAction { + + static readonly CLASS = `${this.LABEL_ACTION_CLASS} prominent config`; + private static readonly HIDE = `${this.CLASS} hide`; + + constructor( + @IMcpService private readonly mcpService: IMcpService, + @IMcpRegistry private readonly mcpRegistry: IMcpRegistry, + @IEditorService private readonly editorService: IEditorService, + ) { + super('extensions.config', localize('config', "Show Configuration"), ShowServerConfigurationAction.CLASS, false); + this.update(); + } + + update(): void { + this.enabled = false; + this.class = ShowServerConfigurationAction.HIDE; + const configurationTarget = this.getConfigurationTarget(); + if (!configurationTarget) { + return; + } + this.class = ShowServerConfigurationAction.CLASS; + this.enabled = true; + this.label = localize('config', "Show Configuration"); + } + + override async run(): Promise { + const configurationTarget = this.getConfigurationTarget(); + if (!configurationTarget) { + return; + } + this.editorService.openEditor({ + resource: URI.isUri(configurationTarget) ? configurationTarget : configurationTarget!.uri, + options: { selection: URI.isUri(configurationTarget) ? undefined : configurationTarget!.range } + }); + } + + private getConfigurationTarget(): Location | URI | undefined { + if (!this.mcpServer) { + return; + } + if (!this.mcpServer.local) { + return; + } + const server = this.mcpService.servers.get().find(s => s.definition.label === this.mcpServer?.name); + if (!server) { + return; + } + const collection = this.mcpRegistry.collections.get().find(c => c.id === server.collection.id); + const serverDefinition = collection?.serverDefinitions.get().find(s => s.id === server.definition.id); + return serverDefinition?.presentation?.origin || collection?.presentation?.origin; + } +} diff --git a/src/vs/workbench/contrib/mcp/browser/mcpServerEditor.ts b/src/vs/workbench/contrib/mcp/browser/mcpServerEditor.ts new file mode 100644 index 00000000000..42b1f1c0bb7 --- /dev/null +++ b/src/vs/workbench/contrib/mcp/browser/mcpServerEditor.ts @@ -0,0 +1,684 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { $, Dimension, addDisposableListener, append, clearNode, setParentFlowTo } from '../../../../base/browser/dom.js'; +import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js'; +import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js'; +import { DomScrollableElement } from '../../../../base/browser/ui/scrollbar/scrollableElement.js'; +import { Action, IAction } from '../../../../base/common/actions.js'; +import * as arrays from '../../../../base/common/arrays.js'; +import { Cache, CacheResult } from '../../../../base/common/cache.js'; +import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; +import { isCancellationError } from '../../../../base/common/errors.js'; +import { Emitter, Event } from '../../../../base/common/event.js'; +import { Disposable, DisposableStore, MutableDisposable, dispose, toDisposable } from '../../../../base/common/lifecycle.js'; +import { Schemas, matchesScheme } from '../../../../base/common/network.js'; +import { language } from '../../../../base/common/platform.js'; +import { URI } from '../../../../base/common/uri.js'; +import { generateUuid } from '../../../../base/common/uuid.js'; +import { TokenizationRegistry } from '../../../../editor/common/languages.js'; +import { ILanguageService } from '../../../../editor/common/languages/language.js'; +import { generateTokensCSSForColorMap } from '../../../../editor/common/languages/supports/tokenization.js'; +import { localize } from '../../../../nls.js'; +import { IContextKeyService, IScopedContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { INotificationService } from '../../../../platform/notification/common/notification.js'; +import { IOpenerService } from '../../../../platform/opener/common/opener.js'; +import { IStorageService } from '../../../../platform/storage/common/storage.js'; +import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; +import { IThemeService } from '../../../../platform/theme/common/themeService.js'; +import { EditorPane } from '../../../browser/parts/editor/editorPane.js'; +import { IEditorOpenContext } from '../../../common/editor.js'; +import { DEFAULT_MARKDOWN_STYLES, renderMarkdownDocument } from '../../markdown/browser/markdownDocumentRenderer.js'; +import { IWebview, IWebviewService } from '../../webview/browser/webview.js'; +import { IEditorGroup } from '../../../services/editor/common/editorGroupsService.js'; +import { IExtensionService } from '../../../services/extensions/common/extensions.js'; +import { IHoverService } from '../../../../platform/hover/browser/hover.js'; +import { IWorkbenchMcpServer, McpServerContainers, mcpServerIcon } from '../common/mcpTypes.js'; +import { InstallCountWidget, McpServerWidget, onClick, PublisherWidget, RatingsWidget } from './mcpServerWidgets.js'; +import { DropDownAction, InstallAction, ManageMcpServerAction, UninstallAction } from './mcpServerActions.js'; +import { McpServerEditorInput } from './mcpServerEditorInput.js'; +import { IEditorOptions } from '../../../../platform/editor/common/editor.js'; +import { ILocalMcpServer } from '../../../../platform/mcp/common/mcpManagement.js'; +import { IActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js'; +import { ThemeIcon } from '../../../../base/common/themables.js'; + +const enum McpServerEditorTab { + Readme = 'readme', +} + +function toDateString(date: Date) { + return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}, ${date.toLocaleTimeString(language, { hourCycle: 'h23' })}`; +} + +class NavBar extends Disposable { + + private _onChange = this._register(new Emitter<{ id: string | null; focus: boolean }>()); + get onChange(): Event<{ id: string | null; focus: boolean }> { return this._onChange.event; } + + private _currentId: string | null = null; + get currentId(): string | null { return this._currentId; } + + private actions: Action[]; + private actionbar: ActionBar; + + constructor(container: HTMLElement) { + super(); + const element = append(container, $('.navbar')); + this.actions = []; + this.actionbar = this._register(new ActionBar(element)); + } + + push(id: string, label: string, tooltip: string): void { + const action = new Action(id, label, undefined, true, () => this.update(id, true)); + + action.tooltip = tooltip; + + this.actions.push(action); + this.actionbar.push(action); + + if (this.actions.length === 1) { + this.update(id); + } + } + + clear(): void { + this.actions = dispose(this.actions); + this.actionbar.clear(); + } + + switch(id: string): boolean { + const action = this.actions.find(action => action.id === id); + if (action) { + action.run(); + return true; + } + return false; + } + + private update(id: string, focus?: boolean): void { + this._currentId = id; + this._onChange.fire({ id, focus: !!focus }); + this.actions.forEach(a => a.checked = a.id === id); + } +} + +interface ILayoutParticipant { + layout(): void; +} + +interface IActiveElement { + focus(): void; +} + +interface IExtensionEditorTemplate { + iconContainer: HTMLElement; + name: HTMLElement; + description: HTMLElement; + actionsAndStatusContainer: HTMLElement; + actionBar: ActionBar; + navbar: NavBar; + content: HTMLElement; + header: HTMLElement; + mcpServer: IWorkbenchMcpServer; +} + +const enum WebviewIndex { + Readme, + Changelog +} + +export class McpServerEditor extends EditorPane { + + static readonly ID: string = 'workbench.editor.mcpServer'; + + private readonly _scopedContextKeyService = this._register(new MutableDisposable()); + private template: IExtensionEditorTemplate | undefined; + + private mcpServerReadme: Cache | null; + + // Some action bar items use a webview whose vertical scroll position we track in this map + private initialScrollProgress: Map = new Map(); + + // Spot when an ExtensionEditor instance gets reused for a different extension, in which case the vertical scroll positions must be zeroed + private currentIdentifier: string = ''; + + private layoutParticipants: ILayoutParticipant[] = []; + private readonly contentDisposables = this._register(new DisposableStore()); + private readonly transientDisposables = this._register(new DisposableStore()); + private activeElement: IActiveElement | null = null; + private dimension: Dimension | undefined; + + constructor( + group: IEditorGroup, + @ITelemetryService telemetryService: ITelemetryService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IThemeService themeService: IThemeService, + @INotificationService private readonly notificationService: INotificationService, + @IOpenerService private readonly openerService: IOpenerService, + @IStorageService storageService: IStorageService, + @IExtensionService private readonly extensionService: IExtensionService, + @IWebviewService private readonly webviewService: IWebviewService, + @ILanguageService private readonly languageService: ILanguageService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IHoverService private readonly hoverService: IHoverService, + ) { + super(McpServerEditor.ID, group, telemetryService, themeService, storageService); + this.mcpServerReadme = null; + } + + override get scopedContextKeyService(): IContextKeyService | undefined { + return this._scopedContextKeyService.value; + } + + protected createEditor(parent: HTMLElement): void { + const root = append(parent, $('.extension-editor')); + this._scopedContextKeyService.value = this.contextKeyService.createScoped(root); + this._scopedContextKeyService.value.createKey('inExtensionEditor', true); + + root.tabIndex = 0; // this is required for the focus tracker on the editor + root.style.outline = 'none'; + root.setAttribute('role', 'document'); + const header = append(root, $('.header')); + + const iconContainer = append(header, $('.icon-container')); + + const details = append(header, $('.details')); + const title = append(details, $('.title')); + const name = append(title, $('span.name.clickable', { role: 'heading', tabIndex: 0 })); + this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), name, localize('name', "Extension name"))); + + const subtitle = append(details, $('.subtitle')); + const subTitleEntryContainers: HTMLElement[] = []; + + const publisherContainer = append(subtitle, $('.subtitle-entry')); + subTitleEntryContainers.push(publisherContainer); + const publisherWidget = this.instantiationService.createInstance(PublisherWidget, publisherContainer, false); + + const installCountContainer = append(subtitle, $('.subtitle-entry')); + subTitleEntryContainers.push(installCountContainer); + const installCountWidget = this.instantiationService.createInstance(InstallCountWidget, installCountContainer, false); + + const ratingsContainer = append(subtitle, $('.subtitle-entry')); + subTitleEntryContainers.push(ratingsContainer); + const ratingsWidget = this.instantiationService.createInstance(RatingsWidget, ratingsContainer, false); + + const widgets: McpServerWidget[] = [ + publisherWidget, + installCountWidget, + ratingsWidget, + ]; + + const description = append(details, $('.description')); + + const actions = [ + this.instantiationService.createInstance(InstallAction), + this.instantiationService.createInstance(UninstallAction), + this.instantiationService.createInstance(ManageMcpServerAction, true), + ]; + + const actionsAndStatusContainer = append(details, $('.actions-status-container.mcp-server-actions')); + const actionBar = this._register(new ActionBar(actionsAndStatusContainer, { + actionViewItemProvider: (action: IAction, options: IActionViewItemOptions) => { + if (action instanceof DropDownAction) { + return action.createActionViewItem(options); + } + return undefined; + }, + focusOnlyEnabledItems: true + })); + + actionBar.push(actions, { icon: true, label: true }); + actionBar.setFocusable(true); + // update focusable elements when the enablement of an action changes + this._register(Event.any(...actions.map(a => Event.filter(a.onDidChange, e => e.enabled !== undefined)))(() => { + actionBar.setFocusable(false); + actionBar.setFocusable(true); + })); + + const mcpServerContainers: McpServerContainers = this.instantiationService.createInstance(McpServerContainers, [...actions, ...widgets]); + for (const disposable of [...actions, ...widgets, mcpServerContainers]) { + this._register(disposable); + } + + const onError = Event.chain(actionBar.onDidRun, $ => + $.map(({ error }) => error) + .filter(error => !!error) + ); + + this._register(onError(this.onError, this)); + + const body = append(root, $('.body')); + const navbar = new NavBar(body); + + const content = append(body, $('.content')); + content.id = generateUuid(); // An id is needed for the webview parent flow to + + this.template = { + content, + description, + header, + iconContainer, + name, + navbar, + actionsAndStatusContainer, + actionBar: actionBar, + set mcpServer(mcpServer: IWorkbenchMcpServer) { + mcpServerContainers.mcpServer = mcpServer; + let lastNonEmptySubtitleEntryContainer; + for (const subTitleEntryElement of subTitleEntryContainers) { + subTitleEntryElement.classList.remove('last-non-empty'); + if (subTitleEntryElement.children.length > 0) { + lastNonEmptySubtitleEntryContainer = subTitleEntryElement; + } + } + if (lastNonEmptySubtitleEntryContainer) { + lastNonEmptySubtitleEntryContainer.classList.add('last-non-empty'); + } + } + }; + } + + override async setInput(input: McpServerEditorInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + await super.setInput(input, options, context, token); + if (this.template) { + await this.render(input.mcpServer, this.template, !!options?.preserveFocus); + } + } + + private async render(mcpServer: IWorkbenchMcpServer, template: IExtensionEditorTemplate, preserveFocus: boolean): Promise { + this.activeElement = null; + this.transientDisposables.clear(); + + const token = this.transientDisposables.add(new CancellationTokenSource()).token; + + this.mcpServerReadme = new Cache(() => mcpServer.getReadme(token)); + template.mcpServer = mcpServer; + + clearNode(template.iconContainer); + if (mcpServer.iconUrl) { + const icon = append(template.iconContainer, $('img.icon', { alt: '' })); + this.transientDisposables.add(addDisposableListener(icon, 'error', () => { + clearNode(template.iconContainer); + append(template.iconContainer, $(ThemeIcon.asCSSSelector(mcpServerIcon))); + }, { once: true })); + icon.src = mcpServer.iconUrl; + } else { + append(template.iconContainer, $(ThemeIcon.asCSSSelector(mcpServerIcon))); + } + + template.name.textContent = mcpServer.label; + template.name.classList.toggle('clickable', !!mcpServer.url); + template.description.textContent = mcpServer.description; + if (mcpServer.url) { + this.transientDisposables.add(onClick(template.name, () => this.openerService.open(URI.parse(mcpServer.url!)))); + } + + this.renderNavbar(mcpServer, template, preserveFocus); + } + + private renderNavbar(extension: IWorkbenchMcpServer, template: IExtensionEditorTemplate, preserveFocus: boolean): void { + template.content.innerText = ''; + template.navbar.clear(); + + if (this.currentIdentifier !== extension.id) { + this.initialScrollProgress.clear(); + this.currentIdentifier = extension.id; + } + + template.navbar.push(McpServerEditorTab.Readme, localize('details', "Details"), localize('detailstooltip', "Extension details, rendered from the extension's 'README.md' file")); + if (template.navbar.currentId) { + this.onNavbarChange(extension, { id: template.navbar.currentId, focus: !preserveFocus }, template); + } + template.navbar.onChange(e => this.onNavbarChange(extension, e, template), this, this.transientDisposables); + } + + override clearInput(): void { + this.contentDisposables.clear(); + this.transientDisposables.clear(); + + super.clearInput(); + } + + override focus(): void { + super.focus(); + this.activeElement?.focus(); + } + + showFind(): void { + this.activeWebview?.showFind(); + } + + runFindAction(previous: boolean): void { + this.activeWebview?.runFindAction(previous); + } + + public get activeWebview(): IWebview | undefined { + if (!this.activeElement || !(this.activeElement as IWebview).runFindAction) { + return undefined; + } + return this.activeElement as IWebview; + } + + private onNavbarChange(extension: IWorkbenchMcpServer, { id, focus }: { id: string | null; focus: boolean }, template: IExtensionEditorTemplate): void { + this.contentDisposables.clear(); + template.content.innerText = ''; + this.activeElement = null; + if (id) { + const cts = new CancellationTokenSource(); + this.contentDisposables.add(toDisposable(() => cts.dispose(true))); + this.open(id, extension, template, cts.token) + .then(activeElement => { + if (cts.token.isCancellationRequested) { + return; + } + this.activeElement = activeElement; + if (focus) { + this.focus(); + } + }); + } + } + + private open(id: string, extension: IWorkbenchMcpServer, template: IExtensionEditorTemplate, token: CancellationToken): Promise { + switch (id) { + case McpServerEditorTab.Readme: return this.openDetails(extension, template, token); + } + return Promise.resolve(null); + } + + private async openMarkdown(extension: IWorkbenchMcpServer, cacheResult: CacheResult, noContentCopy: string, container: HTMLElement, webviewIndex: WebviewIndex, title: string, token: CancellationToken): Promise { + try { + const body = await this.renderMarkdown(extension, cacheResult, container, token); + if (token.isCancellationRequested) { + return Promise.resolve(null); + } + + const webview = this.contentDisposables.add(this.webviewService.createWebviewOverlay({ + title, + options: { + enableFindWidget: true, + tryRestoreScrollPosition: true, + disableServiceWorker: true, + }, + contentOptions: {}, + extension: undefined, + })); + + webview.initialScrollProgress = this.initialScrollProgress.get(webviewIndex) || 0; + + webview.claim(this, this.window, this.scopedContextKeyService); + setParentFlowTo(webview.container, container); + webview.layoutWebviewOverElement(container); + + webview.setHtml(body); + webview.claim(this, this.window, undefined); + + this.contentDisposables.add(webview.onDidFocus(() => this._onDidFocus?.fire())); + + this.contentDisposables.add(webview.onDidScroll(() => this.initialScrollProgress.set(webviewIndex, webview.initialScrollProgress))); + + const removeLayoutParticipant = arrays.insert(this.layoutParticipants, { + layout: () => { + webview.layoutWebviewOverElement(container); + } + }); + this.contentDisposables.add(toDisposable(removeLayoutParticipant)); + + let isDisposed = false; + this.contentDisposables.add(toDisposable(() => { isDisposed = true; })); + + this.contentDisposables.add(this.themeService.onDidColorThemeChange(async () => { + // Render again since syntax highlighting of code blocks may have changed + const body = await this.renderMarkdown(extension, cacheResult, container); + if (!isDisposed) { // Make sure we weren't disposed of in the meantime + webview.setHtml(body); + } + })); + + this.contentDisposables.add(webview.onDidClickLink(link => { + if (!link) { + return; + } + // Only allow links with specific schemes + if (matchesScheme(link, Schemas.http) || matchesScheme(link, Schemas.https) || matchesScheme(link, Schemas.mailto)) { + this.openerService.open(link); + } + })); + + return webview; + } catch (e) { + const p = append(container, $('p.nocontent')); + p.textContent = noContentCopy; + return p; + } + } + + private async renderMarkdown(extension: IWorkbenchMcpServer, cacheResult: CacheResult, container: HTMLElement, token?: CancellationToken): Promise { + const contents = await this.loadContents(() => cacheResult, container); + if (token?.isCancellationRequested) { + return ''; + } + + const content = await renderMarkdownDocument(contents, this.extensionService, this.languageService, { shouldSanitize: true, token }); + if (token?.isCancellationRequested) { + return ''; + } + + return this.renderBody(content); + } + + private renderBody(body: string): string { + const nonce = generateUuid(); + const colorMap = TokenizationRegistry.getColorMap(); + const css = colorMap ? generateTokensCSSForColorMap(colorMap) : ''; + return ` + + + + + + + + + ${body} + + `; + } + + private async openDetails(extension: IWorkbenchMcpServer, template: IExtensionEditorTemplate, token: CancellationToken): Promise { + const details = append(template.content, $('.details')); + const readmeContainer = append(details, $('.readme-container')); + const additionalDetailsContainer = append(details, $('.additional-details-container')); + + const layout = () => details.classList.toggle('narrow', this.dimension && this.dimension.width < 500); + layout(); + this.contentDisposables.add(toDisposable(arrays.insert(this.layoutParticipants, { layout }))); + + const activeElement = await this.openMarkdown(extension, this.mcpServerReadme!.get(), localize('noReadme', "No README available."), readmeContainer, WebviewIndex.Readme, localize('Readme title', "Readme"), token); + this.renderAdditionalDetails(additionalDetailsContainer, extension); + return activeElement; + } + + private renderAdditionalDetails(container: HTMLElement, extension: IWorkbenchMcpServer): void { + const content = $('div', { class: 'additional-details-content', tabindex: '0' }); + const scrollableContent = new DomScrollableElement(content, {}); + const layout = () => scrollableContent.scanDomNode(); + const removeLayoutParticipant = arrays.insert(this.layoutParticipants, { layout }); + this.contentDisposables.add(toDisposable(removeLayoutParticipant)); + this.contentDisposables.add(scrollableContent); + + this.contentDisposables.add(this.instantiationService.createInstance(AdditionalDetailsWidget, content, extension)); + + append(container, scrollableContent.getDomNode()); + scrollableContent.scanDomNode(); + } + + private loadContents(loadingTask: () => CacheResult, container: HTMLElement): Promise { + container.classList.add('loading'); + + const result = this.contentDisposables.add(loadingTask()); + const onDone = () => container.classList.remove('loading'); + result.promise.then(onDone, onDone); + + return result.promise; + } + + layout(dimension: Dimension): void { + this.dimension = dimension; + this.layoutParticipants.forEach(p => p.layout()); + } + + private onError(err: any): void { + if (isCancellationError(err)) { + return; + } + + this.notificationService.error(err); + } +} + +class AdditionalDetailsWidget extends Disposable { + + private readonly disposables = this._register(new DisposableStore()); + + constructor( + private readonly container: HTMLElement, + extension: IWorkbenchMcpServer, + @IHoverService private readonly hoverService: IHoverService, + @IOpenerService private readonly openerService: IOpenerService, + ) { + super(); + this.render(extension); + } + + private render(extension: IWorkbenchMcpServer): void { + this.container.innerText = ''; + this.disposables.clear(); + + if (extension.local) { + this.renderInstallInfo(this.container, extension.local); + } + + if (extension.gallery) { + this.renderMarketplaceInfo(this.container, extension); + } + this.renderExtensionResources(this.container, extension); + } + + private renderExtensionResources(container: HTMLElement, extension: IWorkbenchMcpServer): void { + const resources: [string, URI][] = []; + if (extension.repository) { + try { + resources.push([localize('repository', "Repository"), URI.parse(extension.repository)]); + } catch (error) {/* Ignore */ } + } + if (extension.publisherUrl && extension.publisherDisplayName) { + resources.push([extension.publisherDisplayName, URI.parse(extension.publisherUrl)]); + } + if (resources.length) { + const extensionResourcesContainer = append(container, $('.resources-container.additional-details-element')); + append(extensionResourcesContainer, $('.additional-details-title', undefined, localize('resources', "Resources"))); + const resourcesElement = append(extensionResourcesContainer, $('.resources')); + for (const [label, uri] of resources) { + const resource = append(resourcesElement, $('a.resource', { tabindex: '0' }, label)); + this.disposables.add(onClick(resource, () => this.openerService.open(uri))); + this.disposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), resource, uri.toString())); + } + } + } + + private renderInstallInfo(container: HTMLElement, extension: ILocalMcpServer): void { + const installInfoContainer = append(container, $('.more-info-container.additional-details-element')); + append(installInfoContainer, $('.additional-details-title', undefined, localize('Install Info', "Installation"))); + const installInfo = append(installInfoContainer, $('.more-info')); + append(installInfo, + $('.more-info-entry', undefined, + $('div.more-info-entry-name', undefined, localize('id', "Identifier")), + $('code', undefined, extension.name) + )); + append(installInfo, + $('.more-info-entry', undefined, + $('div.more-info-entry-name', undefined, localize('Version', "Version")), + $('code', undefined, extension.version) + ) + ); + } + + private renderMarketplaceInfo(container: HTMLElement, extension: IWorkbenchMcpServer): void { + const gallery = extension.gallery; + const moreInfoContainer = append(container, $('.more-info-container.additional-details-element')); + append(moreInfoContainer, $('.additional-details-title', undefined, localize('Marketplace Info', "Marketplace"))); + const moreInfo = append(moreInfoContainer, $('.more-info')); + if (gallery) { + if (!extension.local) { + append(moreInfo, + $('.more-info-entry', undefined, + $('div.more-info-entry-name', undefined, localize('id', "Identifier")), + $('code', undefined, extension.name) + )); + append(moreInfo, + $('.more-info-entry', undefined, + $('div.more-info-entry-name', undefined, localize('Version', "Version")), + $('code', undefined, gallery.version) + ) + ); + } + append(moreInfo, + $('.more-info-entry', undefined, + $('div.more-info-entry-name', undefined, localize('last released', "Last Released")), + $('div', undefined, toDateString(new Date(gallery.lastUpdated))) + ) + ); + } + } +} diff --git a/src/vs/workbench/contrib/mcp/browser/mcpServerEditorInput.ts b/src/vs/workbench/contrib/mcp/browser/mcpServerEditorInput.ts new file mode 100644 index 00000000000..18a9416a0bc --- /dev/null +++ b/src/vs/workbench/contrib/mcp/browser/mcpServerEditorInput.ts @@ -0,0 +1,59 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Schemas } from '../../../../base/common/network.js'; +import { URI } from '../../../../base/common/uri.js'; +import { localize } from '../../../../nls.js'; +import { EditorInputCapabilities, IUntypedEditorInput } from '../../../common/editor.js'; +import { EditorInput } from '../../../common/editor/editorInput.js'; +import { join } from '../../../../base/common/path.js'; +import { ThemeIcon } from '../../../../base/common/themables.js'; +import { Codicon } from '../../../../base/common/codicons.js'; +import { registerIcon } from '../../../../platform/theme/common/iconRegistry.js'; +import { IWorkbenchMcpServer } from '../common/mcpTypes.js'; + +const ExtensionEditorIcon = registerIcon('extensions-editor-label-icon', Codicon.extensions, localize('extensionsEditorLabelIcon', 'Icon of the extensions editor label.')); + +export class McpServerEditorInput extends EditorInput { + + static readonly ID = 'workbench.mcpServer.input2'; + + override get typeId(): string { + return McpServerEditorInput.ID; + } + + override get capabilities(): EditorInputCapabilities { + return EditorInputCapabilities.Readonly | EditorInputCapabilities.Singleton; + } + + override get resource() { + return URI.from({ + scheme: Schemas.extension, + path: join(this.mcpServer.id, 'mcpServer') + }); + } + + constructor(private _mcpServer: IWorkbenchMcpServer) { + super(); + } + + get mcpServer(): IWorkbenchMcpServer { return this._mcpServer; } + + override getName(): string { + return localize('extensionsInputName', "Extension: {0}", this._mcpServer.label); + } + + override getIcon(): ThemeIcon | undefined { + return ExtensionEditorIcon; + } + + override matches(other: EditorInput | IUntypedEditorInput): boolean { + if (super.matches(other)) { + return true; + } + + return other instanceof McpServerEditorInput && this._mcpServer.name === other._mcpServer.name; + } +} diff --git a/src/vs/workbench/contrib/mcp/browser/mcpServerWidgets.ts b/src/vs/workbench/contrib/mcp/browser/mcpServerWidgets.ts new file mode 100644 index 00000000000..716afc112f3 --- /dev/null +++ b/src/vs/workbench/contrib/mcp/browser/mcpServerWidgets.ts @@ -0,0 +1,252 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from '../../../../base/browser/dom.js'; +import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js'; +import { IManagedHover } from '../../../../base/browser/ui/hover/hover.js'; +import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js'; +import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; +import { KeyCode } from '../../../../base/common/keyCodes.js'; +import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; +import { ThemeIcon } from '../../../../base/common/themables.js'; +import * as platform from '../../../../base/common/platform.js'; +import { URI } from '../../../../base/common/uri.js'; +import { localize } from '../../../../nls.js'; +import { IHoverService } from '../../../../platform/hover/browser/hover.js'; +import { IOpenerService } from '../../../../platform/opener/common/opener.js'; +import { verifiedPublisherIcon } from '../../../services/extensionManagement/common/extensionsIcons.js'; +import { installCountIcon, starEmptyIcon, starFullIcon, starHalfIcon } from '../../extensions/browser/extensionsIcons.js'; +import { IMcpServerContainer, IWorkbenchMcpServer } from '../common/mcpTypes.js'; + +export abstract class McpServerWidget extends Disposable implements IMcpServerContainer { + private _mcpServer: IWorkbenchMcpServer | null = null; + get mcpServer(): IWorkbenchMcpServer | null { return this._mcpServer; } + set mcpServer(mcpServer: IWorkbenchMcpServer | null) { this._mcpServer = mcpServer; this.update(); } + update(): void { this.render(); } + abstract render(): void; +} + +export function onClick(element: HTMLElement, callback: () => void): IDisposable { + const disposables: DisposableStore = new DisposableStore(); + disposables.add(dom.addDisposableListener(element, dom.EventType.CLICK, dom.finalHandler(callback))); + disposables.add(dom.addDisposableListener(element, dom.EventType.KEY_UP, e => { + const keyboardEvent = new StandardKeyboardEvent(e); + if (keyboardEvent.equals(KeyCode.Space) || keyboardEvent.equals(KeyCode.Enter)) { + e.preventDefault(); + e.stopPropagation(); + callback(); + } + })); + return disposables; +} + +export class PublisherWidget extends McpServerWidget { + + private element: HTMLElement | undefined; + private containerHover: IManagedHover | undefined; + + private readonly disposables = this._register(new DisposableStore()); + + constructor( + readonly container: HTMLElement, + private small: boolean, + @IHoverService private readonly hoverService: IHoverService, + @IOpenerService private readonly openerService: IOpenerService, + ) { + super(); + + this.render(); + this._register(toDisposable(() => this.clear())); + } + + private clear(): void { + this.element?.remove(); + this.disposables.clear(); + } + + render(): void { + this.clear(); + if (!this.mcpServer?.publisherDisplayName) { + return; + } + + this.element = dom.append(this.container, dom.$('.publisher')); + const publisherDisplayName = dom.$('.publisher-name.ellipsis'); + publisherDisplayName.textContent = this.mcpServer.publisherDisplayName; + + const verifiedPublisher = dom.$('.verified-publisher'); + dom.append(verifiedPublisher, dom.$('span.extension-verified-publisher.clickable'), renderIcon(verifiedPublisherIcon)); + + if (this.small) { + if (this.mcpServer.gallery?.publisherDomain?.verified) { + dom.append(this.element, verifiedPublisher); + } + dom.append(this.element, publisherDisplayName); + } else { + this.element.setAttribute('role', 'button'); + this.element.tabIndex = 0; + + this.containerHover = this.disposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.element, localize('publisher', "Publisher ({0})", this.mcpServer.publisherDisplayName))); + dom.append(this.element, publisherDisplayName); + + if (this.mcpServer.gallery?.publisherDomain?.verified) { + dom.append(this.element, verifiedPublisher); + const publisherDomainLink = URI.parse(this.mcpServer.gallery?.publisherDomain.link); + verifiedPublisher.tabIndex = 0; + verifiedPublisher.setAttribute('role', 'button'); + this.containerHover.update(localize('verified publisher', "This publisher has verified ownership of {0}", this.mcpServer.gallery?.publisherDomain.link)); + verifiedPublisher.setAttribute('role', 'link'); + + dom.append(verifiedPublisher, dom.$('span.extension-verified-publisher-domain', undefined, publisherDomainLink.authority.startsWith('www.') ? publisherDomainLink.authority.substring(4) : publisherDomainLink.authority)); + this.disposables.add(onClick(verifiedPublisher, () => this.openerService.open(publisherDomainLink))); + } + } + + } + +} + +export class InstallCountWidget extends McpServerWidget { + + private readonly disposables = this._register(new DisposableStore()); + + constructor( + readonly container: HTMLElement, + private small: boolean, + @IHoverService private readonly hoverService: IHoverService, + ) { + super(); + this.render(); + + this._register(toDisposable(() => this.clear())); + } + + private clear(): void { + this.container.innerText = ''; + this.disposables.clear(); + } + + render(): void { + this.clear(); + + if (!this.mcpServer?.installCount) { + return; + } + + const installLabel = InstallCountWidget.getInstallLabel(this.mcpServer, this.small); + if (!installLabel) { + return; + } + + const parent = this.small ? this.container : dom.append(this.container, dom.$('span.install', { tabIndex: 0 })); + dom.append(parent, dom.$('span' + ThemeIcon.asCSSSelector(installCountIcon))); + const count = dom.append(parent, dom.$('span.count')); + count.textContent = installLabel; + + if (!this.small) { + this.disposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.container, localize('install count', "Install count"))); + } + } + + static getInstallLabel(extension: IWorkbenchMcpServer, small: boolean): string | undefined { + const installCount = extension.installCount; + + if (!installCount) { + return undefined; + } + + let installLabel: string; + + if (small) { + if (installCount > 1000000) { + installLabel = `${Math.floor(installCount / 100000) / 10}M`; + } else if (installCount > 1000) { + installLabel = `${Math.floor(installCount / 1000)}K`; + } else { + installLabel = String(installCount); + } + } + else { + installLabel = installCount.toLocaleString(platform.language); + } + + return installLabel; + } +} + +export class RatingsWidget extends McpServerWidget { + + private containerHover: IManagedHover | undefined; + private readonly disposables = this._register(new DisposableStore()); + + constructor( + readonly container: HTMLElement, + private small: boolean, + @IHoverService private readonly hoverService: IHoverService, + ) { + super(); + container.classList.add('extension-ratings'); + + if (this.small) { + container.classList.add('small'); + } + + this.render(); + this._register(toDisposable(() => this.clear())); + } + + private clear(): void { + this.container.innerText = ''; + this.disposables.clear(); + } + + render(): void { + this.clear(); + + if (!this.mcpServer) { + return; + } + + if (this.mcpServer.rating === undefined) { + return; + } + + if (this.small && !this.mcpServer.ratingCount) { + return; + } + + if (!this.mcpServer.url) { + return; + } + + const rating = Math.round(this.mcpServer.rating * 2) / 2; + if (this.small) { + dom.append(this.container, dom.$('span' + ThemeIcon.asCSSSelector(starFullIcon))); + + const count = dom.append(this.container, dom.$('span.count')); + count.textContent = String(rating); + } else { + const element = dom.append(this.container, dom.$('span.rating.clickable', { tabIndex: 0 })); + for (let i = 1; i <= 5; i++) { + if (rating >= i) { + dom.append(element, dom.$('span' + ThemeIcon.asCSSSelector(starFullIcon))); + } else if (rating >= i - 0.5) { + dom.append(element, dom.$('span' + ThemeIcon.asCSSSelector(starHalfIcon))); + } else { + dom.append(element, dom.$('span' + ThemeIcon.asCSSSelector(starEmptyIcon))); + } + } + if (this.mcpServer.ratingCount) { + const ratingCountElement = dom.append(element, dom.$('span', undefined, ` (${this.mcpServer.ratingCount})`)); + ratingCountElement.style.paddingLeft = '1px'; + } + + this.containerHover = this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), element, '')); + this.containerHover.update(localize('ratedLabel', "Average rating: {0} out of 5", rating)); + element.setAttribute('role', 'link'); + } + } + +} diff --git a/src/vs/workbench/contrib/mcp/browser/mcpServersView.ts b/src/vs/workbench/contrib/mcp/browser/mcpServersView.ts new file mode 100644 index 00000000000..c257e7b9fbe --- /dev/null +++ b/src/vs/workbench/contrib/mcp/browser/mcpServersView.ts @@ -0,0 +1,248 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from '../../../../base/browser/dom.js'; +import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js'; +import { IListContextMenuEvent, IListRenderer } from '../../../../base/browser/ui/list/list.js'; +import { Event } from '../../../../base/common/event.js'; +import { combinedDisposable, DisposableStore, dispose, IDisposable, isDisposable } from '../../../../base/common/lifecycle.js'; +import { DelayedPagedModel, IPagedModel, PagedModel } from '../../../../base/common/paging.js'; +import { localize } from '../../../../nls.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; +import { IHoverService } from '../../../../platform/hover/browser/hover.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; +import { WorkbenchPagedList } from '../../../../platform/list/browser/listService.js'; +import { INotificationService } from '../../../../platform/notification/common/notification.js'; +import { IOpenerService } from '../../../../platform/opener/common/opener.js'; +import { IThemeService } from '../../../../platform/theme/common/themeService.js'; +import { getLocationBasedViewColors, ViewPane } from '../../../browser/parts/views/viewPane.js'; +import { IViewletViewOptions } from '../../../browser/parts/views/viewsViewlet.js'; +import { IViewDescriptorService } from '../../../common/views.js'; +import { IMcpWorkbenchService, IWorkbenchMcpServer, McpServerContainers, mcpServerIcon } from '../common/mcpTypes.js'; +import { DropDownAction, InstallAction, ManageMcpServerAction } from './mcpServerActions.js'; +import { PublisherWidget, InstallCountWidget, RatingsWidget } from './mcpServerWidgets.js'; +import { ActionRunner, IAction, Separator } from '../../../../base/common/actions.js'; +import { IActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js'; +import { ThemeIcon } from '../../../../base/common/themables.js'; + +export class McpServersListView extends ViewPane { + + private list: WorkbenchPagedList | null = null; + private readonly contextMenuActionRunner = this._register(new ActionRunner()); + + constructor( + options: IViewletViewOptions, + @IKeybindingService keybindingService: IKeybindingService, + @IContextMenuService contextMenuService: IContextMenuService, + @IInstantiationService instantiationService: IInstantiationService, + @IThemeService themeService: IThemeService, + @IHoverService hoverService: IHoverService, + @IConfigurationService configurationService: IConfigurationService, + @IContextKeyService contextKeyService: IContextKeyService, + @IViewDescriptorService viewDescriptorService: IViewDescriptorService, + @IOpenerService openerService: IOpenerService, + @IMcpWorkbenchService private readonly mcpWorkbenchService: IMcpWorkbenchService, + ) { + super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService); + } + + protected override renderBody(container: HTMLElement): void { + super.renderBody(container); + + const mcpServersList = dom.append(container, dom.$('.mcp-servers-list')); + this.list = this._register(this.instantiationService.createInstance(WorkbenchPagedList, + `${this.id}-MCP-Servers`, + mcpServersList, + { + getHeight() { return 72; }, + getTemplateId: () => McpServerRenderer.templateId, + }, + [this.instantiationService.createInstance(McpServerRenderer)], + { + multipleSelectionSupport: false, + setRowLineHeight: false, + horizontalScrolling: false, + accessibilityProvider: { + getAriaLabel(mcpServer: IWorkbenchMcpServer | null): string { + return mcpServer?.label ?? ''; + }, + getWidgetAriaLabel(): string { + return localize('mcp servers', "MCP Servers"); + } + }, + overrideStyles: getLocationBasedViewColors(this.viewDescriptorService.getViewLocationById(this.id)).listOverrideStyles, + openOnSingleClick: true, + }) as WorkbenchPagedList); + this._register(Event.debounce(Event.filter(this.list.onDidOpen, e => e.element !== null), (_, event) => event, 75, true)(options => { + this.mcpWorkbenchService.open(options.element!, options.editorOptions); + })); + this._register(this.list.onContextMenu(e => this.onContextMenu(e), this)); + } + + private async onContextMenu(e: IListContextMenuEvent): Promise { + if (e.element) { + const disposables = new DisposableStore(); + const manageExtensionAction = disposables.add(this.instantiationService.createInstance(ManageMcpServerAction, false)); + const extension = e.element ? this.mcpWorkbenchService.local.find(local => local.name === e.element!.name) || e.element + : e.element; + manageExtensionAction.mcpServer = extension; + let groups: IAction[][] = []; + if (manageExtensionAction.enabled) { + groups = await manageExtensionAction.getActionGroups(); + } + const actions: IAction[] = []; + for (const menuActions of groups) { + for (const menuAction of menuActions) { + actions.push(menuAction); + if (isDisposable(menuAction)) { + disposables.add(menuAction); + } + } + actions.push(new Separator()); + } + actions.pop(); + this.contextMenuService.showContextMenu({ + getAnchor: () => e.anchor, + getActions: () => actions, + actionRunner: this.contextMenuActionRunner, + onHide: () => disposables.dispose() + }); + } + } + + protected override layoutBody(height: number, width: number): void { + super.layoutBody(height, width); + this.list?.layout(height, width); + } + + async show(query: string): Promise> { + if (!this.list) { + return new PagedModel([]); + } + + query = query.trim(); + const servers = query ? await this.mcpWorkbenchService.queryGallery({ text: query.replace('@mcp', '') }) : await this.mcpWorkbenchService.queryLocal(); + this.list.model = new DelayedPagedModel(new PagedModel(servers)); + return this.list.model; + } + +} + +interface IMcpServerTemplateData { + root: HTMLElement; + element: HTMLElement; + icon: HTMLElement; + name: HTMLElement; + description: HTMLElement; + installCount: HTMLElement; + ratings: HTMLElement; + mcpServer: IWorkbenchMcpServer | null; + disposables: IDisposable[]; + mcpServerDisposables: IDisposable[]; + actionbar: ActionBar; +} + +class McpServerRenderer implements IListRenderer { + + static readonly templateId = 'mcpServer'; + readonly templateId = McpServerRenderer.templateId; + + constructor( + @IInstantiationService private readonly instantiationService: IInstantiationService, + @INotificationService private readonly notificationService: INotificationService, + ) { } + + renderTemplate(root: HTMLElement): IMcpServerTemplateData { + const element = dom.append(root, dom.$('.mcp-server-item.extension-list-item')); + const icon = dom.append(element, dom.$('.icon-container')); + const details = dom.append(element, dom.$('.details')); + const headerContainer = dom.append(details, dom.$('.header-container')); + const header = dom.append(headerContainer, dom.$('.header')); + const name = dom.append(header, dom.$('span.name')); + const installCount = dom.append(header, dom.$('span.install-count')); + const ratings = dom.append(header, dom.$('span.ratings')); + const description = dom.append(details, dom.$('.description.ellipsis')); + const footer = dom.append(details, dom.$('.footer')); + const publisherWidget = this.instantiationService.createInstance(PublisherWidget, dom.append(footer, dom.$('.publisher-container')), true); + const actionbar = new ActionBar(footer, { + actionViewItemProvider: (action: IAction, options: IActionViewItemOptions) => { + if (action instanceof DropDownAction) { + return action.createActionViewItem(options); + } + return undefined; + }, + focusOnlyEnabledItems: true + }); + + actionbar.setFocusable(false); + const actionBarListener = actionbar.onDidRun(({ error }) => error && this.notificationService.error(error)); + + const actions = [ + this.instantiationService.createInstance(InstallAction), + this.instantiationService.createInstance(ManageMcpServerAction, false), + ]; + + const widgets = [ + publisherWidget, + this.instantiationService.createInstance(InstallCountWidget, installCount, true), + this.instantiationService.createInstance(RatingsWidget, ratings, true), + ]; + const extensionContainers: McpServerContainers = this.instantiationService.createInstance(McpServerContainers, [...actions, ...widgets]); + + actionbar.push(actions, { icon: true, label: true }); + const disposable = combinedDisposable(...actions, ...widgets, actionbar, actionBarListener, extensionContainers); + + return { + root, element, icon, name, description, installCount, ratings, disposables: [disposable], actionbar, + mcpServerDisposables: [], + set mcpServer(mcpServer: IWorkbenchMcpServer) { + extensionContainers.mcpServer = mcpServer; + } + }; + } + + renderElement(mcpServer: IWorkbenchMcpServer, index: number, data: IMcpServerTemplateData): void { + data.element.classList.remove('loading'); + data.mcpServerDisposables = dispose(data.mcpServerDisposables); + data.root.setAttribute('data-mcp-server-id', mcpServer.id); + + dom.clearNode(data.icon); + if (mcpServer.iconUrl) { + const icon = dom.append(data.icon, dom.$('img.icon', { alt: '' })); + data.mcpServerDisposables.push(dom.addDisposableListener(icon, 'error', () => { + dom.clearNode(data.icon); + dom.append(data.icon, dom.$(ThemeIcon.asCSSSelector(mcpServerIcon))); + }, { once: true })); + icon.src = mcpServer.iconUrl; + if (!icon.complete) { + data.icon.style.visibility = 'hidden'; + icon.onload = () => icon.style.visibility = 'inherit'; + } else { + icon.style.visibility = 'inherit'; + } + } else { + dom.append(data.icon, dom.$(ThemeIcon.asCSSSelector(mcpServerIcon))); + } + + data.name.textContent = mcpServer.label; + data.description.textContent = mcpServer.description; + + data.installCount.style.display = ''; + data.ratings.style.display = ''; + data.mcpServer = mcpServer; + } + + disposeElement(mcpServer: IWorkbenchMcpServer, index: number, data: IMcpServerTemplateData): void { + data.mcpServerDisposables = dispose(data.mcpServerDisposables); + } + + disposeTemplate(data: IMcpServerTemplateData): void { + data.mcpServerDisposables = dispose(data.mcpServerDisposables); + data.disposables = dispose(data.disposables); + } +} diff --git a/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts b/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts new file mode 100644 index 00000000000..c109e6fcc38 --- /dev/null +++ b/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts @@ -0,0 +1,200 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { Emitter } from '../../../../base/common/event.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { IEditorOptions } from '../../../../platform/editor/common/editor.js'; +import { IFileService } from '../../../../platform/files/common/files.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { DidUninstallMcpServerEvent, IGalleryMcpServer, ILocalMcpServer, IMcpGalleryService, IMcpManagementService, InstallMcpServerResult, IQueryOptions } from '../../../../platform/mcp/common/mcpManagement.js'; +import { IWorkbenchContribution } from '../../../common/contributions.js'; +import { ACTIVE_GROUP, IEditorService } from '../../../services/editor/common/editorService.js'; +import { HasInstalledMcpServersContext, IMcpWorkbenchService, IWorkbenchMcpServer, McpServersGalleryEnabledContext } from '../common/mcpTypes.js'; +import { McpServerEditorInput } from './mcpServerEditorInput.js'; + +class McpWorkbenchServer implements IWorkbenchMcpServer { + + constructor( + public local: ILocalMcpServer | undefined, + public gallery: IGalleryMcpServer | undefined, + @IMcpGalleryService private readonly mcpGalleryService: IMcpGalleryService, + @IFileService private readonly fileService: IFileService, + ) { + } + + get id(): string { + return this.gallery?.id ?? this.local?.id ?? ''; + } + + get name(): string { + return this.gallery?.name ?? this.local?.name ?? ''; + } + + get label(): string { + return this.gallery?.displayName ?? this.local?.displayName ?? ''; + } + + get iconUrl(): string | undefined { + return this.gallery?.iconUrl ?? this.local?.iconUrl; + } + + get publisherDisplayName(): string | undefined { + return this.gallery?.publisherDisplayName ?? this.local?.publisherDisplayName ?? this.gallery?.publisher ?? this.local?.publisher; + } + + get publisherUrl(): string | undefined { + return this.gallery?.publisherDomain?.link; + } + + get description(): string { + return this.gallery?.description ?? this.local?.description ?? ''; + } + + get installCount(): number { + return this.gallery?.installCount ?? 0; + } + + get url(): string | undefined { + return this.gallery?.url; + } + + get repository(): string | undefined { + return this.gallery?.repositoryUrl; + } + + async getReadme(token: CancellationToken): Promise { + if (this.local?.readmeUrl) { + const content = await this.fileService.readFile(this.local.readmeUrl); + return content.value.toString(); + } + + if (this.gallery?.readmeUrl) { + return this.mcpGalleryService.getReadme(this.gallery, token); + } + + return Promise.reject(new Error('not available')); + } + +} + +export class McpWorkbenchService extends Disposable implements IMcpWorkbenchService { + + _serviceBrand: undefined; + + private _local: McpWorkbenchServer[] = []; + get local(): readonly McpWorkbenchServer[] { return this._local; } + + private readonly _onChange = this._register(new Emitter()); + readonly onChange = this._onChange.event; + + constructor( + @IMcpGalleryService private readonly mcpGalleryService: IMcpGalleryService, + @IMcpManagementService private readonly mcpManagementService: IMcpManagementService, + @IEditorService private readonly editorService: IEditorService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { + super(); + this._register(this.mcpManagementService.onDidInstallMcpServers(e => this.onDidInstallMcpServers(e))); + this._register(this.mcpManagementService.onDidUninstallMcpServer(e => this.onDidUninstallMcpServer(e))); + this.queryLocal().then(async () => { + await this.queryGallery(); + this._onChange.fire(undefined); + }); + } + + private onDidUninstallMcpServer(e: DidUninstallMcpServerEvent) { + if (e.error) { + return; + } + const server = this._local.find(server => server.local?.name === e.name); + if (server) { + this._local = this._local.filter(server => server.local?.name !== e.name); + server.local = undefined; + this._onChange.fire(server); + } + } + + private onDidInstallMcpServers(e: readonly InstallMcpServerResult[]) { + for (const result of e) { + if (!result.local) { + continue; + } + let server = this._local.find(server => server.local?.name === result.name); + if (server) { + server.local = result.local; + } else { + server = this.instantiationService.createInstance(McpWorkbenchServer, result.local, result.source); + this._local.push(server); + } + this._onChange.fire(server); + } + } + + private fromGallery(gallery: IGalleryMcpServer): IWorkbenchMcpServer | undefined { + for (const local of this._local) { + if (local.name === gallery.name) { + local.gallery = gallery; + return local; + } + } + return undefined; + } + + async queryGallery(options?: IQueryOptions, token?: CancellationToken): Promise { + if (!this.mcpGalleryService.isEnabled()) { + return []; + } + const result = await this.mcpGalleryService.query(options, token); + return result.map(gallery => this.fromGallery(gallery) ?? this.instantiationService.createInstance(McpWorkbenchServer, undefined, gallery)); + } + + async queryLocal(): Promise { + const installed = await this.mcpManagementService.getInstalled(); + this._local = installed.map(i => { + const local = this._local.find(server => server.name === i.name) ?? this.instantiationService.createInstance(McpWorkbenchServer, undefined, undefined); + local.local = i; + return local; + }); + return this._local; + } + + async install(server: IWorkbenchMcpServer): Promise { + if (!server.gallery) { + throw new Error('Gallery server is missing'); + } + await this.mcpManagementService.installFromGallery(server.gallery, server.gallery.packageTypes[0]); + } + + async uninstall(server: IWorkbenchMcpServer): Promise { + if (!server.local) { + throw new Error('Local server is missing'); + } + await this.mcpManagementService.uninstall(server.local); + } + + async open(extension: IWorkbenchMcpServer, options?: IEditorOptions): Promise { + await this.editorService.openEditor(this.instantiationService.createInstance(McpServerEditorInput, extension), options, ACTIVE_GROUP); + } + +} + + +export class MCPContextsInitialisation extends Disposable implements IWorkbenchContribution { + + static ID = 'workbench.mcp.contexts.initialisation'; + + constructor( + @IMcpWorkbenchService mcpWorkbenchService: IMcpWorkbenchService, + @IMcpGalleryService mcpGalleryService: IMcpGalleryService, + @IContextKeyService contextKeyService: IContextKeyService, + ) { + super(); + const hasInstalledMcpServersContextKey = HasInstalledMcpServersContext.bindTo(contextKeyService); + McpServersGalleryEnabledContext.bindTo(contextKeyService).set(mcpGalleryService.isEnabled()); + this._register(mcpWorkbenchService.onChange(() => hasInstalledMcpServersContextKey.set(mcpWorkbenchService.local.length > 0))); + } +} diff --git a/src/vs/workbench/contrib/mcp/common/mcpCommandIds.ts b/src/vs/workbench/contrib/mcp/common/mcpCommandIds.ts index f76e2408b12..019b9b8e54a 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpCommandIds.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpCommandIds.ts @@ -19,5 +19,6 @@ export const enum McpCommandIds { RestartServer = 'workbench.mcp.restartServer', StartServer = 'workbench.mcp.startServer', StopServer = 'workbench.mcp.stopServer', - InstallFromActivation = 'workbench.mcp.installFromActivation' + InstallFromActivation = 'workbench.mcp.installFromActivation', + Browse = 'workbench.mcp.browseServers' } diff --git a/src/vs/workbench/contrib/mcp/common/mcpRegistryInputStorage.ts b/src/vs/workbench/contrib/mcp/common/mcpRegistryInputStorage.ts index 91f473aafda..82a63933f8a 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpRegistryInputStorage.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpRegistryInputStorage.ts @@ -163,9 +163,9 @@ export class McpRegistryInputStorage extends Disposable { const encrypted = decodeBase64(this._record.value.secrets.value); const decrypted = await crypto.subtle.decrypt( - { name: MCP_ENCRYPTION_KEY_ALGORITHM, iv: iv.buffer }, + { name: MCP_ENCRYPTION_KEY_ALGORITHM, iv: iv.buffer as Uint8Array }, key, - encrypted.buffer, + encrypted.buffer as Uint8Array, ); const unsealedSecrets = JSON.parse(new TextDecoder().decode(decrypted)); diff --git a/src/vs/workbench/contrib/mcp/common/mcpService.ts b/src/vs/workbench/contrib/mcp/common/mcpService.ts index 4ea7121fc91..8444b698a25 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpService.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpService.ts @@ -107,7 +107,6 @@ export class McpService extends Disposable implements IMcpService { userDescription: tool.definition.description ?? '', inputSchema: tool.definition.inputSchema, canBeReferencedInPrompt: true, - supportsToolPicker: true, alwaysDisplayInputOutput: true, runsInWorkspace: collection?.scope === StorageScope.WORKSPACE || !!collection?.remoteAuthority, tags: ['mcp'], diff --git a/src/vs/workbench/contrib/mcp/common/mcpTypes.ts b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts index f43dc1472ff..0af3b59b424 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpTypes.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts @@ -4,9 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import { equals as arraysEqual } from '../../../../base/common/arrays.js'; +import { Event } from '../../../../base/common/event.js'; import { assertNever } from '../../../../base/common/assert.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; -import { IDisposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js'; import { equals as objectsEqual } from '../../../../base/common/objects.js'; import { IObservable } from '../../../../base/common/observable.js'; import { URI, UriComponents } from '../../../../base/common/uri.js'; @@ -20,6 +21,11 @@ import { IWorkspaceFolderData } from '../../../../platform/workspace/common/work import { ToolProgress } from '../../chat/common/languageModelToolsService.js'; import { McpServerRequestHandler } from './mcpServerRequestHandler.js'; import { MCP } from './modelContextProtocol.js'; +import { IGalleryMcpServer, ILocalMcpServer, IQueryOptions } from '../../../../platform/mcp/common/mcpManagement.js'; +import { IEditorOptions } from '../../../../platform/editor/common/editor.js'; +import { RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; +import { registerIcon } from '../../../../platform/theme/common/iconRegistry.js'; +import { Codicon } from '../../../../base/common/codicons.js'; export const extensionMcpCollectionPrefix = 'ext.'; @@ -437,3 +443,69 @@ export class MpcResponseError extends Error { } export class McpConnectionFailedError extends Error { } + +export interface IMcpServerContainer extends IDisposable { + mcpServer: IWorkbenchMcpServer | null; + update(): void; +} + +export interface IWorkbenchMcpServer { + readonly gallery: IGalleryMcpServer | undefined; + readonly local: ILocalMcpServer | undefined; + readonly id: string; + readonly name: string; + readonly label: string; + readonly description: string; + readonly iconUrl?: string; + readonly publisherUrl?: string; + readonly publisherDisplayName?: string; + readonly installCount?: number; + readonly ratingCount?: number; + readonly rating?: number; + readonly url?: string; + readonly repository?: string; + getReadme(token: CancellationToken): Promise; +} + +export const IMcpWorkbenchService = createDecorator('IMcpWorkbenchService'); +export interface IMcpWorkbenchService { + readonly _serviceBrand: undefined; + readonly onChange: Event; + readonly local: readonly IWorkbenchMcpServer[]; + queryLocal(): Promise; + queryGallery(options?: IQueryOptions, token?: CancellationToken): Promise; + install(mcpServer: IWorkbenchMcpServer): Promise; + uninstall(mcpServer: IWorkbenchMcpServer): Promise; + open(extension: IWorkbenchMcpServer | string, options?: IEditorOptions): Promise; +} + +export class McpServerContainers extends Disposable { + constructor( + private readonly containers: IMcpServerContainer[], + @IMcpWorkbenchService mcpWorkbenchService: IMcpWorkbenchService + ) { + super(); + this._register(mcpWorkbenchService.onChange(this.update, this)); + } + + set mcpServer(extension: IWorkbenchMcpServer | null) { + this.containers.forEach(c => c.mcpServer = extension); + } + + update(server: IWorkbenchMcpServer | undefined): void { + for (const container of this.containers) { + if (server && container.mcpServer) { + if (server.name === container.mcpServer.name) { + container.mcpServer = server; + } + } else { + container.update(); + } + } + } +} + +export const McpServersGalleryEnabledContext = new RawContextKey('mcpServersGalleryEnabled', false); +export const HasInstalledMcpServersContext = new RawContextKey('hasInstalledMcpServers', false); +export const InstalledMcpServersViewId = 'workbench.views.mcp.installed'; +export const mcpServerIcon = registerIcon('mcp-server', Codicon.tools, localize('mcpServer', 'Icon used for the MCP server.')); diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts index 4a10caf524f..73a22e6d97c 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts @@ -41,16 +41,9 @@ export class MergeEditorInput extends AbstractTextResourceEditorInput implements private _inputModel?: IMergeEditorInputModel; - private _focusedEditor: MergeEditorType = 'result'; + private _focusedEditor: MergeEditorType; - override closeHandler: IEditorCloseHandler = { - showConfirm: () => this._inputModel?.shouldConfirmClose() ?? false, - confirm: async (editors) => { - assertFn(() => editors.every(e => e.editor instanceof MergeEditorInput)); - const inputModels = editors.map(e => (e.editor as MergeEditorInput)._inputModel).filter(isDefined); - return await this._inputModel!.confirmClose(inputModels); - }, - }; + override closeHandler: IEditorCloseHandler; private get useWorkingCopy() { return this.configurationService.getValue('mergeEditor.useWorkingCopy') ?? false; @@ -73,6 +66,21 @@ export class MergeEditorInput extends AbstractTextResourceEditorInput implements @ILogService private readonly logService: ILogService, ) { super(result, undefined, editorService, textFileService, labelService, fileService, filesConfigurationService, textResourceConfigurationService, customEditorLabelService); + this._focusedEditor = 'result'; + this.closeHandler = { + showConfirm: () => this._inputModel?.shouldConfirmClose() ?? false, + confirm: async (editors) => { + assertFn(() => editors.every(e => e.editor instanceof MergeEditorInput)); + const inputModels = editors.map(e => (e.editor as MergeEditorInput)._inputModel).filter(isDefined); + return await this._inputModel!.confirmClose(inputModels); + }, + }; + this.mergeEditorModeFactory = this._instaService.createInstance( + this.useWorkingCopy + ? TempFileMergeEditorModeFactory + : WorkspaceMergeEditorModeFactory, + this._instaService.createInstance(MergeEditorTelemetry), + ); } override dispose(): void { @@ -99,12 +107,7 @@ export class MergeEditorInput extends AbstractTextResourceEditorInput implements return localize('name', "Merging: {0}", super.getName()); } - private readonly mergeEditorModeFactory = this._instaService.createInstance( - this.useWorkingCopy - ? TempFileMergeEditorModeFactory - : WorkspaceMergeEditorModeFactory, - this._instaService.createInstance(MergeEditorTelemetry), - ); + private readonly mergeEditorModeFactory; override async resolve(): Promise { if (!this._inputModel) { diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInputModel.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInputModel.ts index a110faf6a99..634cf4e3792 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInputModel.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInputModel.ts @@ -127,16 +127,12 @@ export class TempFileMergeEditorModeFactory implements IMergeEditorInputModelFac } class TempFileMergeEditorInputModel extends EditorModel implements IMergeEditorInputModel { - private readonly savedAltVersionId = observableValue(this, this.model.resultTextModel.getAlternativeVersionId()); - private readonly altVersionId = observableFromEvent(this, - e => this.model.resultTextModel.onDidChangeContent(e), - () => - /** @description getAlternativeVersionId */ this.model.resultTextModel.getAlternativeVersionId() - ); + private readonly savedAltVersionId; + private readonly altVersionId; - public readonly isDirty = derived(this, (reader) => this.altVersionId.read(reader) !== this.savedAltVersionId.read(reader)); + public readonly isDirty; - private finished = false; + private finished; constructor( public readonly model: MergeEditorModel, @@ -148,6 +144,13 @@ class TempFileMergeEditorInputModel extends EditorModel implements IMergeEditorI @IEditorService private readonly editorService: IEditorService, ) { super(); + this.savedAltVersionId = observableValue(this, this.model.resultTextModel.getAlternativeVersionId()); + this.altVersionId = observableFromEvent(this, + e => this.model.resultTextModel.onDidChangeContent(e), + () => /** @description getAlternativeVersionId */ this.model.resultTextModel.getAlternativeVersionId() + ); + this.isDirty = derived(this, (reader) => this.altVersionId.read(reader) !== this.savedAltVersionId.read(reader)); + this.finished = false; } override dispose(): void { @@ -359,13 +362,10 @@ export class WorkspaceMergeEditorModeFactory implements IMergeEditorInputModelFa } class WorkspaceMergeEditorInputModel extends EditorModel implements IMergeEditorInputModel { - public readonly isDirty = observableFromEvent(this, - Event.any(this.resultTextFileModel.onDidChangeDirty, this.resultTextFileModel.onDidSaveError), - () => /** @description isDirty */ this.resultTextFileModel.isDirty() - ); + public readonly isDirty; - private reported = false; - private readonly dateTimeOpened = new Date(); + private reported; + private readonly dateTimeOpened; constructor( public readonly model: MergeEditorModel, @@ -376,6 +376,12 @@ class WorkspaceMergeEditorInputModel extends EditorModel implements IMergeEditor @IStorageService private readonly _storageService: IStorageService, ) { super(); + this.isDirty = observableFromEvent(this, + Event.any(this.resultTextFileModel.onDidChangeDirty, this.resultTextFileModel.onDidSaveError), + () => /** @description isDirty */ this.resultTextFileModel.isDirty() + ); + this.reported = false; + this.dateTimeOpened = new Date(); } public override dispose(): void { diff --git a/src/vs/workbench/contrib/mergeEditor/browser/model/diffComputer.ts b/src/vs/workbench/contrib/mergeEditor/browser/model/diffComputer.ts index 93bc4dcc95c..e6a51aed0c0 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/model/diffComputer.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/model/diffComputer.ts @@ -23,14 +23,15 @@ export interface IMergeDiffComputerResult { } export class MergeDiffComputer implements IMergeDiffComputer { - private readonly mergeAlgorithm = observableConfigValue<'smart' | 'experimental' | 'legacy' | 'advanced'>( - 'mergeEditor.diffAlgorithm', 'advanced', this.configurationService) - .map(v => v === 'smart' ? 'legacy' : v === 'experimental' ? 'advanced' : v); + private readonly mergeAlgorithm; constructor( @IEditorWorkerService private readonly editorWorkerService: IEditorWorkerService, @IConfigurationService private readonly configurationService: IConfigurationService, ) { + this.mergeAlgorithm = observableConfigValue<'smart' | 'experimental' | 'legacy' | 'advanced'>( + 'mergeEditor.diffAlgorithm', 'advanced', this.configurationService) + .map(v => v === 'smart' ? 'legacy' : v === 'experimental' ? 'advanced' : v); } async computeDiff(textModel1: ITextModel, textModel2: ITextModel, reader: IReader): Promise { diff --git a/src/vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel.ts b/src/vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel.ts index 30d2ad96b65..87ae59790cb 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel.ts @@ -29,25 +29,14 @@ export interface InputData { } export class MergeEditorModel extends EditorModel { - private readonly input1TextModelDiffs = this._register(new TextModelDiffs(this.base, this.input1.textModel, this.diffComputer)); - private readonly input2TextModelDiffs = this._register(new TextModelDiffs(this.base, this.input2.textModel, this.diffComputer)); - private readonly resultTextModelDiffs = this._register(new TextModelDiffs(this.base, this.resultTextModel, this.diffComputer)); - public readonly modifiedBaseRanges = derived(this, (reader) => { - const input1Diffs = this.input1TextModelDiffs.diffs.read(reader); - const input2Diffs = this.input2TextModelDiffs.diffs.read(reader); - return ModifiedBaseRange.fromDiffs(input1Diffs, input2Diffs, this.base, this.input1.textModel, this.input2.textModel); - }); + private readonly input1TextModelDiffs; + private readonly input2TextModelDiffs; + private readonly resultTextModelDiffs; + public readonly modifiedBaseRanges; - private readonly modifiedBaseRangeResultStates = derived(this, reader => { - const map = new Map( - this.modifiedBaseRanges.read(reader).map<[ModifiedBaseRange, ModifiedBaseRangeData]>((s) => [ - s, new ModifiedBaseRangeData(s) - ]) - ); - return map; - }); + private readonly modifiedBaseRangeResultStates; - private readonly resultSnapshot = this.resultTextModel.createSnapshot(); + private readonly resultSnapshot; constructor( readonly base: ITextModel, @@ -61,6 +50,101 @@ export class MergeEditorModel extends EditorModel { @IUndoRedoService private readonly undoRedoService: IUndoRedoService, ) { super(); + this.input1TextModelDiffs = this._register(new TextModelDiffs(this.base, this.input1.textModel, this.diffComputer)); + this.input2TextModelDiffs = this._register(new TextModelDiffs(this.base, this.input2.textModel, this.diffComputer)); + this.resultTextModelDiffs = this._register(new TextModelDiffs(this.base, this.resultTextModel, this.diffComputer)); + this.modifiedBaseRanges = derived(this, (reader) => { + const input1Diffs = this.input1TextModelDiffs.diffs.read(reader); + const input2Diffs = this.input2TextModelDiffs.diffs.read(reader); + return ModifiedBaseRange.fromDiffs(input1Diffs, input2Diffs, this.base, this.input1.textModel, this.input2.textModel); + }); + this.modifiedBaseRangeResultStates = derived(this, reader => { + const map = new Map( + this.modifiedBaseRanges.read(reader).map<[ModifiedBaseRange, ModifiedBaseRangeData]>((s) => [ + s, new ModifiedBaseRangeData(s) + ]) + ); + return map; + }); + this.resultSnapshot = this.resultTextModel.createSnapshot(); + this.baseInput1Diffs = this.input1TextModelDiffs.diffs; + this.baseInput2Diffs = this.input2TextModelDiffs.diffs; + this.baseResultDiffs = this.resultTextModelDiffs.diffs; + this.input1ResultMapping = derived(this, reader => { + return this.getInputResultMapping( + this.baseInput1Diffs.read(reader), + this.baseResultDiffs.read(reader), + this.input1.textModel.getLineCount(), + ); + }); + this.resultInput1Mapping = derived(this, reader => this.input1ResultMapping.read(reader).reverse()); + this.input2ResultMapping = derived(this, reader => { + return this.getInputResultMapping( + this.baseInput2Diffs.read(reader), + this.baseResultDiffs.read(reader), + this.input2.textModel.getLineCount(), + ); + }); + this.resultInput2Mapping = derived(this, reader => this.input2ResultMapping.read(reader).reverse()); + this.baseResultMapping = derived(this, reader => { + const map = new DocumentLineRangeMap(this.baseResultDiffs.read(reader), -1); + return new DocumentLineRangeMap( + map.lineRangeMappings.map((m) => + m.inputRange.isEmpty || m.outputRange.isEmpty + ? new LineRangeMapping( + // We can do this because two adjacent diffs have one line in between. + m.inputRange.deltaStart(-1), + m.outputRange.deltaStart(-1) + ) + : m + ), + map.inputLineCount + ); + }); + this.resultBaseMapping = derived(this, reader => this.baseResultMapping.read(reader).reverse()); + this.diffComputingState = derived(this, reader => { + const states = [ + this.input1TextModelDiffs, + this.input2TextModelDiffs, + this.resultTextModelDiffs, + ].map((s) => s.state.read(reader)); + + if (states.some((s) => s === TextModelDiffState.initializing)) { + return MergeEditorModelState.initializing; + } + if (states.some((s) => s === TextModelDiffState.updating)) { + return MergeEditorModelState.updating; + } + return MergeEditorModelState.upToDate; + }); + this.inputDiffComputingState = derived(this, reader => { + const states = [ + this.input1TextModelDiffs, + this.input2TextModelDiffs, + ].map((s) => s.state.read(reader)); + + if (states.some((s) => s === TextModelDiffState.initializing)) { + return MergeEditorModelState.initializing; + } + if (states.some((s) => s === TextModelDiffState.updating)) { + return MergeEditorModelState.updating; + } + return MergeEditorModelState.upToDate; + }); + this.isUpToDate = derived(this, reader => this.diffComputingState.read(reader) === MergeEditorModelState.upToDate); + + this.firstRun = true; + this.unhandledConflictsCount = derived(this, reader => { + const map = this.modifiedBaseRangeResultStates.read(reader); + let unhandledCount = 0; + for (const [_key, value] of map) { + if (!value.handled.read(reader)) { + unhandledCount++; + } + } + return unhandledCount; + }); + this.hasUnhandledConflicts = this.unhandledConflictsCount.map(value => /** @description hasUnhandledConflicts */ value > 0); this._register(keepObserved(this.modifiedBaseRangeResultStates)); this._register(keepObserved(this.input1ResultMapping)); @@ -68,7 +152,7 @@ export class MergeEditorModel extends EditorModel { const initializePromise = this.initialize(); - this.onInitialized = this.onInitialized.then(async () => { + this.onInitialized = waitForState(this.diffComputingState, state => state === MergeEditorModelState.upToDate).then(async () => { await initializePromise; }); @@ -202,30 +286,18 @@ export class MergeEditorModel extends EditorModel { return this.modifiedBaseRangeResultStates.get().has(baseRange); } - public readonly baseInput1Diffs = this.input1TextModelDiffs.diffs; + public readonly baseInput1Diffs; - public readonly baseInput2Diffs = this.input2TextModelDiffs.diffs; - public readonly baseResultDiffs = this.resultTextModelDiffs.diffs; + public readonly baseInput2Diffs; + public readonly baseResultDiffs; public get isApplyingEditInResult(): boolean { return this.resultTextModelDiffs.isApplyingChange; } - public readonly input1ResultMapping = derived(this, reader => { - return this.getInputResultMapping( - this.baseInput1Diffs.read(reader), - this.baseResultDiffs.read(reader), - this.input1.textModel.getLineCount(), - ); - }); + public readonly input1ResultMapping; - public readonly resultInput1Mapping = derived(this, reader => this.input1ResultMapping.read(reader).reverse()); + public readonly resultInput1Mapping; - public readonly input2ResultMapping = derived(this, reader => { - return this.getInputResultMapping( - this.baseInput2Diffs.read(reader), - this.baseResultDiffs.read(reader), - this.input2.textModel.getLineCount(), - ); - }); + public readonly input2ResultMapping; - public readonly resultInput2Mapping = derived(this, reader => this.input2ResultMapping.read(reader).reverse()); + public readonly resultInput2Mapping; private getInputResultMapping(inputLinesDiffs: DetailedLineRangeMapping[], resultDiffs: DetailedLineRangeMapping[], inputLineCount: number) { const map = DocumentLineRangeMap.betweenOutputs(inputLinesDiffs, resultDiffs, inputLineCount); @@ -243,23 +315,9 @@ export class MergeEditorModel extends EditorModel { ); } - public readonly baseResultMapping = derived(this, reader => { - const map = new DocumentLineRangeMap(this.baseResultDiffs.read(reader), -1); - return new DocumentLineRangeMap( - map.lineRangeMappings.map((m) => - m.inputRange.isEmpty || m.outputRange.isEmpty - ? new LineRangeMapping( - // We can do this because two adjacent diffs have one line in between. - m.inputRange.deltaStart(-1), - m.outputRange.deltaStart(-1) - ) - : m - ), - map.inputLineCount - ); - }); + public readonly baseResultMapping; - public readonly resultBaseMapping = derived(this, reader => this.baseResultMapping.read(reader).reverse()); + public readonly resultBaseMapping; public translateInputRangeToBase(input: 1 | 2, range: Range): Range { const baseInputDiffs = input === 1 ? this.baseInput1Diffs.get() : this.baseInput2Diffs.get(); @@ -292,42 +350,15 @@ export class MergeEditorModel extends EditorModel { return this.modifiedBaseRanges.get().filter(r => r.baseRange.intersectsOrTouches(rangeInBase)); } - public readonly diffComputingState = derived(this, reader => { - const states = [ - this.input1TextModelDiffs, - this.input2TextModelDiffs, - this.resultTextModelDiffs, - ].map((s) => s.state.read(reader)); + public readonly diffComputingState; - if (states.some((s) => s === TextModelDiffState.initializing)) { - return MergeEditorModelState.initializing; - } - if (states.some((s) => s === TextModelDiffState.updating)) { - return MergeEditorModelState.updating; - } - return MergeEditorModelState.upToDate; - }); + public readonly inputDiffComputingState; - public readonly inputDiffComputingState = derived(this, reader => { - const states = [ - this.input1TextModelDiffs, - this.input2TextModelDiffs, - ].map((s) => s.state.read(reader)); + public readonly isUpToDate; - if (states.some((s) => s === TextModelDiffState.initializing)) { - return MergeEditorModelState.initializing; - } - if (states.some((s) => s === TextModelDiffState.updating)) { - return MergeEditorModelState.updating; - } - return MergeEditorModelState.upToDate; - }); + public readonly onInitialized; - public readonly isUpToDate = derived(this, reader => this.diffComputingState.read(reader) === MergeEditorModelState.upToDate); - - public readonly onInitialized = waitForState(this.diffComputingState, state => state === MergeEditorModelState.upToDate).then(() => { }); - - private firstRun = true; + private firstRun; private updateBaseRangeAcceptedState(resultDiffs: DetailedLineRangeMapping[], states: Map, tx: ITransaction): void { const baseRangeWithStoreAndTouchingDiffs = leftJoin( states, @@ -543,18 +574,9 @@ export class MergeEditorModel extends EditorModel { state.handledInput2.set(handled, tx); } - public readonly unhandledConflictsCount = derived(this, reader => { - const map = this.modifiedBaseRangeResultStates.read(reader); - let unhandledCount = 0; - for (const [_key, value] of map) { - if (!value.handled.read(reader)) { - unhandledCount++; - } - } - return unhandledCount; - }); + public readonly unhandledConflictsCount; - public readonly hasUnhandledConflicts = this.unhandledConflictsCount.map(value => /** @description hasUnhandledConflicts */ value > 0); + public readonly hasUnhandledConflicts; public setLanguageId(languageId: string, source?: string): void { const language = this.languageService.createById(languageId); @@ -758,16 +780,23 @@ function arrayCount(array: Iterable, predicate: (value: T) => boolean): nu } class ModifiedBaseRangeData { - constructor(private readonly baseRange: ModifiedBaseRange) { } + constructor(private readonly baseRange: ModifiedBaseRange) { + this.accepted = observableValue(`BaseRangeState${this.baseRange.baseRange}`, ModifiedBaseRangeState.base); + this.handledInput1 = observableValue(`BaseRangeHandledState${this.baseRange.baseRange}.Input1`, false); + this.handledInput2 = observableValue(`BaseRangeHandledState${this.baseRange.baseRange}.Input2`, false); + this.computedFromDiffing = false; + this.previousNonDiffingState = undefined; + this.handled = derived(this, reader => this.handledInput1.read(reader) && this.handledInput2.read(reader)); + } - public accepted: ISettableObservable = observableValue(`BaseRangeState${this.baseRange.baseRange}`, ModifiedBaseRangeState.base); - public handledInput1: ISettableObservable = observableValue(`BaseRangeHandledState${this.baseRange.baseRange}.Input1`, false); - public handledInput2: ISettableObservable = observableValue(`BaseRangeHandledState${this.baseRange.baseRange}.Input2`, false); + public accepted: ISettableObservable; + public handledInput1: ISettableObservable; + public handledInput2: ISettableObservable; - public computedFromDiffing = false; - public previousNonDiffingState: ModifiedBaseRangeState | undefined = undefined; + public computedFromDiffing; + public previousNonDiffingState: ModifiedBaseRangeState | undefined; - public readonly handled = derived(this, reader => this.handledInput1.read(reader) && this.handledInput2.read(reader)); + public readonly handled; } export const enum MergeEditorModelState { diff --git a/src/vs/workbench/contrib/mergeEditor/browser/model/modifiedBaseRange.ts b/src/vs/workbench/contrib/mergeEditor/browser/model/modifiedBaseRange.ts index d223dc3fd09..09ffd04e8c8 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/model/modifiedBaseRange.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/model/modifiedBaseRange.ts @@ -44,9 +44,9 @@ export class ModifiedBaseRange { ); } - public readonly input1CombinedDiff = DetailedLineRangeMapping.join(this.input1Diffs); - public readonly input2CombinedDiff = DetailedLineRangeMapping.join(this.input2Diffs); - public readonly isEqualChange = equals(this.input1Diffs, this.input2Diffs, (a, b) => a.getLineEdit().equals(b.getLineEdit())); + public readonly input1CombinedDiff; + public readonly input2CombinedDiff; + public readonly isEqualChange; constructor( public readonly baseRange: MergeEditorLineRange, @@ -66,6 +66,13 @@ export class ModifiedBaseRange { */ public readonly input2Diffs: readonly DetailedLineRangeMapping[] ) { + this.input1CombinedDiff = DetailedLineRangeMapping.join(this.input1Diffs); + this.input2CombinedDiff = DetailedLineRangeMapping.join(this.input2Diffs); + this.isEqualChange = equals(this.input1Diffs, this.input2Diffs, (a, b) => a.getLineEdit().equals(b.getLineEdit())); + this.smartInput1LineRangeEdit = null; + this.smartInput2LineRangeEdit = null; + this.dumbInput1LineRangeEdit = null; + this.dumbInput2LineRangeEdit = null; if (this.input1Diffs.length === 0 && this.input2Diffs.length === 0) { throw new BugIndicatingError('must have at least one diff'); } @@ -135,8 +142,8 @@ export class ModifiedBaseRange { }; } - private smartInput1LineRangeEdit: LineRangeEdit | undefined | null = null; - private smartInput2LineRangeEdit: LineRangeEdit | undefined | null = null; + private smartInput1LineRangeEdit: LineRangeEdit | undefined | null; + private smartInput2LineRangeEdit: LineRangeEdit | undefined | null; private smartCombineInputs(firstInput: 1 | 2): LineRangeEdit | undefined { if (firstInput === 1 && this.smartInput1LineRangeEdit !== null) { @@ -173,8 +180,8 @@ export class ModifiedBaseRange { return result; } - private dumbInput1LineRangeEdit: LineRangeEdit | undefined | null = null; - private dumbInput2LineRangeEdit: LineRangeEdit | undefined | null = null; + private dumbInput1LineRangeEdit: LineRangeEdit | undefined | null; + private dumbInput2LineRangeEdit: LineRangeEdit | undefined | null; private dumbCombineInputs(firstInput: 1 | 2): LineRangeEdit | undefined { if (firstInput === 1 && this.dumbInput1LineRangeEdit !== null) { diff --git a/src/vs/workbench/contrib/mergeEditor/browser/model/rangeUtils.ts b/src/vs/workbench/contrib/mergeEditor/browser/model/rangeUtils.ts index 0c6666af32b..ecd6578ad31 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/model/rangeUtils.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/model/rangeUtils.ts @@ -5,7 +5,7 @@ import { Position } from '../../../../../editor/common/core/position.js'; import { Range } from '../../../../../editor/common/core/range.js'; -import { TextLength } from '../../../../../editor/common/core/textLength.js'; +import { TextLength } from '../../../../../editor/common/core/text/textLength.js'; export function rangeContainsPosition(range: Range, position: Position): boolean { if (position.lineNumber < range.startLineNumber || position.lineNumber > range.endLineNumber) { diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/editorGutter.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/editorGutter.ts index b2bea7169cb..7f92d3f354c 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/editorGutter.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/editorGutter.ts @@ -10,19 +10,13 @@ import { CodeEditorWidget } from '../../../../../editor/browser/widget/codeEdito import { MergeEditorLineRange } from '../model/lineRange.js'; export class EditorGutter extends Disposable { - private readonly scrollTop = observableFromEvent(this, - this._editor.onDidScrollChange, - (e) => /** @description editor.onDidScrollChange */ this._editor.getScrollTop() - ); - private readonly isScrollTopZero = this.scrollTop.map((scrollTop) => /** @description isScrollTopZero */ scrollTop === 0); - private readonly modelAttached = observableFromEvent(this, - this._editor.onDidChangeModel, - (e) => /** @description editor.onDidChangeModel */ this._editor.hasModel() - ); + private readonly scrollTop; + private readonly isScrollTopZero; + private readonly modelAttached; - private readonly editorOnDidChangeViewZones = observableSignalFromEvent('onDidChangeViewZones', this._editor.onDidChangeViewZones); - private readonly editorOnDidContentSizeChange = observableSignalFromEvent('onDidContentSizeChange', this._editor.onDidContentSizeChange); - private readonly domNodeSizeChanged = observableSignal('domNodeSizeChanged'); + private readonly editorOnDidChangeViewZones; + private readonly editorOnDidContentSizeChange; + private readonly domNodeSizeChanged; constructor( private readonly _editor: CodeEditorWidget, @@ -30,6 +24,19 @@ export class EditorGutter extends D private readonly itemProvider: IGutterItemProvider ) { super(); + this.scrollTop = observableFromEvent(this, + this._editor.onDidScrollChange, + (e) => /** @description editor.onDidScrollChange */ this._editor.getScrollTop() + ); + this.isScrollTopZero = this.scrollTop.map((scrollTop) => /** @description isScrollTopZero */ scrollTop === 0); + this.modelAttached = observableFromEvent(this, + this._editor.onDidChangeModel, + (e) => /** @description editor.onDidChangeModel */ this._editor.hasModel() + ); + this.editorOnDidChangeViewZones = observableSignalFromEvent('onDidChangeViewZones', this._editor.onDidChangeViewZones); + this.editorOnDidContentSizeChange = observableSignalFromEvent('onDidContentSizeChange', this._editor.onDidContentSizeChange); + this.domNodeSizeChanged = observableSignal('domNodeSizeChanged'); + this.views = new Map(); this._domNode.className = 'gutter monaco-editor'; const scrollDecoration = this._domNode.appendChild( h('div.scroll-decoration', { role: 'presentation', ariaHidden: 'true', style: { width: '100%' } }) @@ -59,7 +66,7 @@ export class EditorGutter extends D reset(this._domNode); } - private readonly views = new Map(); + private readonly views; private render(reader: IReader): void { if (!this.modelAttached.read(reader)) { diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts index 5520e2a0d72..5a2d17c919f 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts @@ -25,76 +25,31 @@ import { observableConfigValue } from '../../../../../../platform/observable/com import { MergeEditorViewModel } from '../viewModel.js'; export abstract class CodeEditorView extends Disposable { - readonly model = this.viewModel.map(m => /** @description model */ m?.model); + readonly model; - protected readonly htmlElements = h('div.code-view', [ - h('div.header@header', [ - h('span.title@title'), - h('span.description@description'), - h('span.detail@detail'), - h('span.toolbar@toolbar'), - ]), - h('div.container', [ - h('div.gutter@gutterDiv'), - h('div@editor'), - ]), - ]); + protected readonly htmlElements; - private readonly _onDidViewChange = new Emitter(); + private readonly _onDidViewChange; - public readonly view: IView = { - element: this.htmlElements.root, - minimumWidth: DEFAULT_EDITOR_MIN_DIMENSIONS.width, - maximumWidth: DEFAULT_EDITOR_MAX_DIMENSIONS.width, - minimumHeight: DEFAULT_EDITOR_MIN_DIMENSIONS.height, - maximumHeight: DEFAULT_EDITOR_MAX_DIMENSIONS.height, - onDidChange: this._onDidViewChange.event, - layout: (width: number, height: number, top: number, left: number) => { - setStyle(this.htmlElements.root, { width, height, top, left }); - this.editor.layout({ - width: width - this.htmlElements.gutterDiv.clientWidth, - height: height - this.htmlElements.header.clientHeight, - }); - } - // preferredWidth?: number | undefined; - // preferredHeight?: number | undefined; - // priority?: LayoutPriority | undefined; - // snap?: boolean | undefined; - }; + public readonly view: IView; - protected readonly checkboxesVisible = observableConfigValue('mergeEditor.showCheckboxes', false, this.configurationService); - protected readonly showDeletionMarkers = observableConfigValue('mergeEditor.showDeletionMarkers', true, this.configurationService); - protected readonly useSimplifiedDecorations = observableConfigValue('mergeEditor.useSimplifiedDecorations', false, this.configurationService); + protected readonly checkboxesVisible; + protected readonly showDeletionMarkers; + protected readonly useSimplifiedDecorations; - public readonly editor = this.instantiationService.createInstance( - CodeEditorWidget, - this.htmlElements.editor, - {}, - { - contributions: this.getEditorContributions(), - } - ); + public readonly editor; public updateOptions(newOptions: Readonly): void { this.editor.updateOptions(newOptions); } - public readonly isFocused = observableFromEvent(this, - Event.any(this.editor.onDidBlurEditorWidget, this.editor.onDidFocusEditorWidget), - () => /** @description editor.hasWidgetFocus */ this.editor.hasWidgetFocus() - ); + public readonly isFocused; - public readonly cursorPosition = observableFromEvent(this, - this.editor.onDidChangeCursorPosition, - () => /** @description editor.getPosition */ this.editor.getPosition() - ); + public readonly cursorPosition; - public readonly selection = observableFromEvent(this, - this.editor.onDidChangeCursorSelection, - () => /** @description editor.getSelections */ this.editor.getSelections() - ); + public readonly selection; - public readonly cursorLineNumber = this.cursorPosition.map(p => /** @description cursorPosition.lineNumber */ p?.lineNumber); + public readonly cursorLineNumber; constructor( private readonly instantiationService: IInstantiationService, @@ -102,6 +57,63 @@ export abstract class CodeEditorView extends Disposable { private readonly configurationService: IConfigurationService, ) { super(); + this.model = this.viewModel.map(m => /** @description model */ m?.model); + this.htmlElements = h('div.code-view', [ + h('div.header@header', [ + h('span.title@title'), + h('span.description@description'), + h('span.detail@detail'), + h('span.toolbar@toolbar'), + ]), + h('div.container', [ + h('div.gutter@gutterDiv'), + h('div@editor'), + ]), + ]); + this._onDidViewChange = new Emitter(); + this.view = { + element: this.htmlElements.root, + minimumWidth: DEFAULT_EDITOR_MIN_DIMENSIONS.width, + maximumWidth: DEFAULT_EDITOR_MAX_DIMENSIONS.width, + minimumHeight: DEFAULT_EDITOR_MIN_DIMENSIONS.height, + maximumHeight: DEFAULT_EDITOR_MAX_DIMENSIONS.height, + onDidChange: this._onDidViewChange.event, + layout: (width: number, height: number, top: number, left: number) => { + setStyle(this.htmlElements.root, { width, height, top, left }); + this.editor.layout({ + width: width - this.htmlElements.gutterDiv.clientWidth, + height: height - this.htmlElements.header.clientHeight, + }); + } + // preferredWidth?: number | undefined; + // preferredHeight?: number | undefined; + // priority?: LayoutPriority | undefined; + // snap?: boolean | undefined; + }; + this.checkboxesVisible = observableConfigValue('mergeEditor.showCheckboxes', false, this.configurationService); + this.showDeletionMarkers = observableConfigValue('mergeEditor.showDeletionMarkers', true, this.configurationService); + this.useSimplifiedDecorations = observableConfigValue('mergeEditor.useSimplifiedDecorations', false, this.configurationService); + this.editor = this.instantiationService.createInstance( + CodeEditorWidget, + this.htmlElements.editor, + {}, + { + contributions: this.getEditorContributions(), + } + ); + this.isFocused = observableFromEvent(this, + Event.any(this.editor.onDidBlurEditorWidget, this.editor.onDidFocusEditorWidget), + () => /** @description editor.hasWidgetFocus */ this.editor.hasWidgetFocus() + ); + this.cursorPosition = observableFromEvent(this, + this.editor.onDidChangeCursorPosition, + () => /** @description editor.getPosition */ this.editor.getPosition() + ); + this.selection = observableFromEvent(this, + this.editor.onDidChangeCursorSelection, + () => /** @description editor.getSelections */ this.editor.getSelections() + ); + this.cursorLineNumber = this.cursorPosition.map(p => /** @description cursorPosition.lineNumber */ p?.lineNumber); } diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/editors/inputCodeEditorView.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/editors/inputCodeEditorView.ts index 4a3b0081cd3..db94b20548a 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/editors/inputCodeEditorView.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/editors/inputCodeEditorView.ts @@ -29,7 +29,7 @@ import { EditorGutter, IGutterItemInfo, IGutterItemView } from '../editorGutter. import { CodeEditorView, createSelectionsAutorun, TitleMenu } from './codeEditorView.js'; export class InputCodeEditorView extends CodeEditorView { - public readonly otherInputNumber = this.inputNumber === 1 ? 2 : 1; + public readonly otherInputNumber; constructor( public readonly inputNumber: 1 | 2, @@ -39,6 +39,119 @@ export class InputCodeEditorView extends CodeEditorView { @IConfigurationService configurationService: IConfigurationService, ) { super(instantiationService, viewModel, configurationService); + this.otherInputNumber = this.inputNumber === 1 ? 2 : 1; + this.modifiedBaseRangeGutterItemInfos = derivedOpts({ debugName: `input${this.inputNumber}.modifiedBaseRangeGutterItemInfos` }, reader => { + const viewModel = this.viewModel.read(reader); + if (!viewModel) { return []; } + const model = viewModel.model; + const inputNumber = this.inputNumber; + + const showNonConflictingChanges = viewModel.showNonConflictingChanges.read(reader); + + return model.modifiedBaseRanges.read(reader) + .filter((r) => r.getInputDiffs(this.inputNumber).length > 0 && (showNonConflictingChanges || r.isConflicting || !model.isHandled(r).read(reader))) + .map((baseRange, idx) => new ModifiedBaseRangeGutterItemModel(idx.toString(), baseRange, inputNumber, viewModel)); + }); + this.decorations = derivedOpts({ debugName: `input${this.inputNumber}.decorations` }, reader => { + const viewModel = this.viewModel.read(reader); + if (!viewModel) { + return []; + } + const model = viewModel.model; + const textModel = (this.inputNumber === 1 ? model.input1 : model.input2).textModel; + + const activeModifiedBaseRange = viewModel.activeModifiedBaseRange.read(reader); + + const result = new Array(); + + const showNonConflictingChanges = viewModel.showNonConflictingChanges.read(reader); + const showDeletionMarkers = this.showDeletionMarkers.read(reader); + const diffWithThis = viewModel.baseCodeEditorView.read(reader) !== undefined && viewModel.baseShowDiffAgainst.read(reader) === this.inputNumber; + const useSimplifiedDecorations = !diffWithThis && this.useSimplifiedDecorations.read(reader); + + for (const modifiedBaseRange of model.modifiedBaseRanges.read(reader)) { + const range = modifiedBaseRange.getInputRange(this.inputNumber); + if (!range) { + continue; + } + + const blockClassNames = ['merge-editor-block']; + let blockPadding: [top: number, right: number, bottom: number, left: number] = [0, 0, 0, 0]; + const isHandled = model.isInputHandled(modifiedBaseRange, this.inputNumber).read(reader); + if (isHandled) { + blockClassNames.push('handled'); + } + if (modifiedBaseRange === activeModifiedBaseRange) { + blockClassNames.push('focused'); + blockPadding = [0, 2, 0, 2]; + } + if (modifiedBaseRange.isConflicting) { + blockClassNames.push('conflicting'); + } + const inputClassName = this.inputNumber === 1 ? 'input i1' : 'input i2'; + blockClassNames.push(inputClassName); + + if (!modifiedBaseRange.isConflicting && !showNonConflictingChanges && isHandled) { + continue; + } + + if (useSimplifiedDecorations && !isHandled) { + blockClassNames.push('use-simplified-decorations'); + } + + result.push({ + range: range.toInclusiveRangeOrEmpty(), + options: { + showIfCollapsed: true, + blockClassName: blockClassNames.join(' '), + blockPadding, + blockIsAfterEnd: range.startLineNumber > textModel.getLineCount(), + description: 'Merge Editor', + minimap: { + position: MinimapPosition.Gutter, + color: { id: isHandled ? handledConflictMinimapOverViewRulerColor : unhandledConflictMinimapOverViewRulerColor }, + }, + overviewRuler: modifiedBaseRange.isConflicting ? { + position: OverviewRulerLane.Center, + color: { id: isHandled ? handledConflictMinimapOverViewRulerColor : unhandledConflictMinimapOverViewRulerColor }, + } : undefined + } + }); + + if (!useSimplifiedDecorations && (modifiedBaseRange.isConflicting || !model.isHandled(modifiedBaseRange).read(reader))) { + const inputDiffs = modifiedBaseRange.getInputDiffs(this.inputNumber); + for (const diff of inputDiffs) { + const range = diff.outputRange.toInclusiveRange(); + if (range) { + result.push({ + range, + options: { + className: `merge-editor-diff ${inputClassName}`, + description: 'Merge Editor', + isWholeLine: true, + } + }); + } + + if (diff.rangeMappings) { + for (const d of diff.rangeMappings) { + if (showDeletionMarkers || !d.outputRange.isEmpty()) { + result.push({ + range: d.outputRange, + options: { + className: d.outputRange.isEmpty() ? `merge-editor-diff-empty-word ${inputClassName}` : `merge-editor-diff-word ${inputClassName}`, + description: 'Merge Editor', + showIfCollapsed: true, + } + }); + } + } + } + } + } + } + return result; + }); this.htmlElements.root.classList.add(`input`); @@ -98,124 +211,14 @@ export class InputCodeEditorView extends CodeEditorView { this._register(applyObservableDecorations(this.editor, this.decorations)); } - private readonly modifiedBaseRangeGutterItemInfos = derivedOpts({ debugName: `input${this.inputNumber}.modifiedBaseRangeGutterItemInfos` }, reader => { - const viewModel = this.viewModel.read(reader); - if (!viewModel) { return []; } - const model = viewModel.model; - const inputNumber = this.inputNumber; + private readonly modifiedBaseRangeGutterItemInfos; - const showNonConflictingChanges = viewModel.showNonConflictingChanges.read(reader); - - return model.modifiedBaseRanges.read(reader) - .filter((r) => r.getInputDiffs(this.inputNumber).length > 0 && (showNonConflictingChanges || r.isConflicting || !model.isHandled(r).read(reader))) - .map((baseRange, idx) => new ModifiedBaseRangeGutterItemModel(idx.toString(), baseRange, inputNumber, viewModel)); - }); - - private readonly decorations = derivedOpts({ debugName: `input${this.inputNumber}.decorations` }, reader => { - const viewModel = this.viewModel.read(reader); - if (!viewModel) { - return []; - } - const model = viewModel.model; - const textModel = (this.inputNumber === 1 ? model.input1 : model.input2).textModel; - - const activeModifiedBaseRange = viewModel.activeModifiedBaseRange.read(reader); - - const result = new Array(); - - const showNonConflictingChanges = viewModel.showNonConflictingChanges.read(reader); - const showDeletionMarkers = this.showDeletionMarkers.read(reader); - const diffWithThis = viewModel.baseCodeEditorView.read(reader) !== undefined && viewModel.baseShowDiffAgainst.read(reader) === this.inputNumber; - const useSimplifiedDecorations = !diffWithThis && this.useSimplifiedDecorations.read(reader); - - for (const modifiedBaseRange of model.modifiedBaseRanges.read(reader)) { - const range = modifiedBaseRange.getInputRange(this.inputNumber); - if (!range) { - continue; - } - - const blockClassNames = ['merge-editor-block']; - let blockPadding: [top: number, right: number, bottom: number, left: number] = [0, 0, 0, 0]; - const isHandled = model.isInputHandled(modifiedBaseRange, this.inputNumber).read(reader); - if (isHandled) { - blockClassNames.push('handled'); - } - if (modifiedBaseRange === activeModifiedBaseRange) { - blockClassNames.push('focused'); - blockPadding = [0, 2, 0, 2]; - } - if (modifiedBaseRange.isConflicting) { - blockClassNames.push('conflicting'); - } - const inputClassName = this.inputNumber === 1 ? 'input i1' : 'input i2'; - blockClassNames.push(inputClassName); - - if (!modifiedBaseRange.isConflicting && !showNonConflictingChanges && isHandled) { - continue; - } - - if (useSimplifiedDecorations && !isHandled) { - blockClassNames.push('use-simplified-decorations'); - } - - result.push({ - range: range.toInclusiveRangeOrEmpty(), - options: { - showIfCollapsed: true, - blockClassName: blockClassNames.join(' '), - blockPadding, - blockIsAfterEnd: range.startLineNumber > textModel.getLineCount(), - description: 'Merge Editor', - minimap: { - position: MinimapPosition.Gutter, - color: { id: isHandled ? handledConflictMinimapOverViewRulerColor : unhandledConflictMinimapOverViewRulerColor }, - }, - overviewRuler: modifiedBaseRange.isConflicting ? { - position: OverviewRulerLane.Center, - color: { id: isHandled ? handledConflictMinimapOverViewRulerColor : unhandledConflictMinimapOverViewRulerColor }, - } : undefined - } - }); - - if (!useSimplifiedDecorations && (modifiedBaseRange.isConflicting || !model.isHandled(modifiedBaseRange).read(reader))) { - const inputDiffs = modifiedBaseRange.getInputDiffs(this.inputNumber); - for (const diff of inputDiffs) { - const range = diff.outputRange.toInclusiveRange(); - if (range) { - result.push({ - range, - options: { - className: `merge-editor-diff ${inputClassName}`, - description: 'Merge Editor', - isWholeLine: true, - } - }); - } - - if (diff.rangeMappings) { - for (const d of diff.rangeMappings) { - if (showDeletionMarkers || !d.outputRange.isEmpty()) { - result.push({ - range: d.outputRange, - options: { - className: d.outputRange.isEmpty() ? `merge-editor-diff-empty-word ${inputClassName}` : `merge-editor-diff-word ${inputClassName}`, - description: 'Merge Editor', - showIfCollapsed: true, - } - }); - } - } - } - } - } - } - return result; - }); + private readonly decorations; } export class ModifiedBaseRangeGutterItemModel implements IGutterItemInfo { - private readonly model = this.viewModel.model; - public readonly range = this.baseRange.getInputRange(this.inputNumber); + private readonly model; + public readonly range; constructor( public readonly id: string, @@ -223,30 +226,35 @@ export class ModifiedBaseRangeGutterItemModel implements IGutterItemInfo { private readonly inputNumber: 1 | 2, private readonly viewModel: MergeEditorViewModel ) { + this.model = this.viewModel.model; + this.range = this.baseRange.getInputRange(this.inputNumber); + this.enabled = this.model.isUpToDate; + this.toggleState = derived(this, reader => { + const input = this.model + .getState(this.baseRange) + .read(reader) + .getInput(this.inputNumber); + return input === InputState.second && !this.baseRange.isOrderRelevant + ? InputState.first + : input; + }); + this.state = derived(this, reader => { + const active = this.viewModel.activeModifiedBaseRange.read(reader); + if (!this.model.hasBaseRange(this.baseRange)) { + return { handled: false, focused: false }; // Invalid state, should only be observed temporarily + } + return { + handled: this.model.isHandled(this.baseRange).read(reader), + focused: this.baseRange === active, + }; + }); } - public readonly enabled = this.model.isUpToDate; + public readonly enabled; - public readonly toggleState: IObservable = derived(this, reader => { - const input = this.model - .getState(this.baseRange) - .read(reader) - .getInput(this.inputNumber); - return input === InputState.second && !this.baseRange.isOrderRelevant - ? InputState.first - : input; - }); + public readonly toggleState: IObservable; - public readonly state: IObservable<{ handled: boolean; focused: boolean }> = derived(this, reader => { - const active = this.viewModel.activeModifiedBaseRange.read(reader); - if (!this.model.hasBaseRange(this.baseRange)) { - return { handled: false, focused: false }; // Invalid state, should only be observed temporarily - } - return { - handled: this.model.isHandled(this.baseRange).read(reader), - focused: this.baseRange === active, - }; - }); + public readonly state: IObservable<{ handled: boolean; focused: boolean }>; public setState(value: boolean, tx: ITransaction): void { this.viewModel.setState( diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/lineAlignment.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/lineAlignment.ts index caf33b64b2b..4bd277e6b71 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/lineAlignment.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/lineAlignment.ts @@ -8,7 +8,7 @@ import { assertFn, checkAdjacentItems } from '../../../../../base/common/assert. import { isDefined } from '../../../../../base/common/types.js'; import { Position } from '../../../../../editor/common/core/position.js'; import { Range } from '../../../../../editor/common/core/range.js'; -import { TextLength } from '../../../../../editor/common/core/textLength.js'; +import { TextLength } from '../../../../../editor/common/core/text/textLength.js'; import { RangeMapping } from '../model/mapping.js'; import { ModifiedBaseRange } from '../model/modifiedBaseRange.js'; import { addLength, lengthBetweenPositions, lengthOfRange } from '../model/rangeUtils.js'; diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts index 6c6e6b5eaf3..ba97c865ba2 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts @@ -56,31 +56,31 @@ export class MergeEditor extends AbstractTextEditor { static readonly ID = 'mergeEditor'; - private readonly _sessionDisposables = new DisposableStore(); - private readonly _viewModel = observableValue(this, undefined); + private readonly _sessionDisposables; + private readonly _viewModel; public get viewModel(): IObservable { return this._viewModel; } private rootHtmlElement: HTMLElement | undefined; - private readonly _grid = this._register(new MutableDisposable>()); - private readonly input1View = this._register(this.instantiationService.createInstance(InputCodeEditorView, 1, this._viewModel)); - private readonly baseView = observableValue(this, undefined); - private readonly baseViewOptions = observableValue | undefined>(this, undefined); - private readonly input2View = this._register(this.instantiationService.createInstance(InputCodeEditorView, 2, this._viewModel)); + private readonly _grid; + private readonly input1View; + private readonly baseView; + private readonly baseViewOptions; + private readonly input2View; - private readonly inputResultView = this._register(this.instantiationService.createInstance(ResultCodeEditorView, this._viewModel)); - private readonly _layoutMode = this.instantiationService.createInstance(MergeEditorLayoutStore); - private readonly _layoutModeObs = observableValue(this, this._layoutMode.value); - private readonly _ctxIsMergeEditor: IContextKey = ctxIsMergeEditor.bindTo(this.contextKeyService); - private readonly _ctxUsesColumnLayout: IContextKey = ctxMergeEditorLayout.bindTo(this.contextKeyService); - private readonly _ctxShowBase: IContextKey = ctxMergeEditorShowBase.bindTo(this.contextKeyService); - private readonly _ctxShowBaseAtTop = ctxMergeEditorShowBaseAtTop.bindTo(this.contextKeyService); - private readonly _ctxResultUri: IContextKey = ctxMergeResultUri.bindTo(this.contextKeyService); - private readonly _ctxBaseUri: IContextKey = ctxMergeBaseUri.bindTo(this.contextKeyService); - private readonly _ctxShowNonConflictingChanges: IContextKey = ctxMergeEditorShowNonConflictingChanges.bindTo(this.contextKeyService); - private readonly _inputModel = observableValue(this, undefined); + private readonly inputResultView; + private readonly _layoutMode; + private readonly _layoutModeObs; + private readonly _ctxIsMergeEditor: IContextKey; + private readonly _ctxUsesColumnLayout: IContextKey; + private readonly _ctxShowBase: IContextKey; + private readonly _ctxShowBaseAtTop; + private readonly _ctxResultUri: IContextKey; + private readonly _ctxBaseUri: IContextKey; + private readonly _ctxShowNonConflictingChanges: IContextKey; + private readonly _inputModel; public get inputModel(): IObservable { return this._inputModel; } @@ -88,13 +88,9 @@ export class MergeEditor extends AbstractTextEditor { return this.inputModel.get()?.model; } - private readonly viewZoneComputer = new ViewZoneComputer( - this.input1View.editor, - this.input2View.editor, - this.inputResultView.editor, - ); + private readonly viewZoneComputer; - private readonly scrollSynchronizer = this._register(new ScrollSynchronizer(this._viewModel, this.input1View, this.input2View, this.baseView, this.inputResultView, this._layoutModeObs)); + private readonly scrollSynchronizer; constructor( group: IEditorGroup, @@ -110,6 +106,35 @@ export class MergeEditor extends AbstractTextEditor { @ICodeEditorService private readonly _codeEditorService: ICodeEditorService ) { super(MergeEditor.ID, group, telemetryService, instantiation, storageService, textResourceConfigurationService, themeService, editorService, editorGroupService, fileService); + this._sessionDisposables = new DisposableStore(); + this._viewModel = observableValue(this, undefined); + this._grid = this._register(new MutableDisposable>()); + this.input1View = this._register(this.instantiationService.createInstance(InputCodeEditorView, 1, this._viewModel)); + this.baseView = observableValue(this, undefined); + this.baseViewOptions = observableValue | undefined>(this, undefined); + this.input2View = this._register(this.instantiationService.createInstance(InputCodeEditorView, 2, this._viewModel)); + this.inputResultView = this._register(this.instantiationService.createInstance(ResultCodeEditorView, this._viewModel)); + this._layoutMode = this.instantiationService.createInstance(MergeEditorLayoutStore); + this._layoutModeObs = observableValue(this, this._layoutMode.value); + this._ctxIsMergeEditor = ctxIsMergeEditor.bindTo(this.contextKeyService); + this._ctxUsesColumnLayout = ctxMergeEditorLayout.bindTo(this.contextKeyService); + this._ctxShowBase = ctxMergeEditorShowBase.bindTo(this.contextKeyService); + this._ctxShowBaseAtTop = ctxMergeEditorShowBaseAtTop.bindTo(this.contextKeyService); + this._ctxResultUri = ctxMergeResultUri.bindTo(this.contextKeyService); + this._ctxBaseUri = ctxMergeBaseUri.bindTo(this.contextKeyService); + this._ctxShowNonConflictingChanges = ctxMergeEditorShowNonConflictingChanges.bindTo(this.contextKeyService); + this._inputModel = observableValue(this, undefined); + this.viewZoneComputer = new ViewZoneComputer( + this.input1View.editor, + this.input2View.editor, + this.inputResultView.editor, + ); + this.scrollSynchronizer = this._register(new ScrollSynchronizer(this._viewModel, this.input1View, this.input2View, this.baseView, this.inputResultView, this._layoutModeObs)); + this._onDidChangeSizeConstraints = new Emitter(); + this.onDidChangeSizeConstraints = this._onDidChangeSizeConstraints.event; + this.baseViewDisposables = this._register(new DisposableStore()); + this.showNonConflictingChangesStore = this.instantiationService.createInstance(PersistentStore, 'mergeEditor/showNonConflictingChanges'); + this.showNonConflictingChanges = observableValue(this, this.showNonConflictingChangesStore.get() ?? false); } override dispose(): void { @@ -122,8 +147,8 @@ export class MergeEditor extends AbstractTextEditor { // #region layout constraints - private readonly _onDidChangeSizeConstraints = new Emitter(); - override readonly onDidChangeSizeConstraints: Event = this._onDidChangeSizeConstraints.event; + private readonly _onDidChangeSizeConstraints; + override readonly onDidChangeSizeConstraints: Event; override get minimumWidth() { return this._layoutMode.value.kind === 'mixed' @@ -552,7 +577,7 @@ export class MergeEditor extends AbstractTextEditor { this.applyLayout(newLayout); } - private readonly baseViewDisposables = this._register(new DisposableStore()); + private readonly baseViewDisposables; private applyLayout(layout: IMergeEditorLayout): void { transaction(tx => { @@ -679,8 +704,8 @@ export class MergeEditor extends AbstractTextEditor { return input instanceof MergeEditorInput; } - private readonly showNonConflictingChangesStore = this.instantiationService.createInstance(PersistentStore, 'mergeEditor/showNonConflictingChanges'); - private readonly showNonConflictingChanges = observableValue(this, this.showNonConflictingChangesStore.get() ?? false); + private readonly showNonConflictingChangesStore; + private readonly showNonConflictingChanges; public toggleShowNonConflictingChanges(): void { this.showNonConflictingChanges.set(!this.showNonConflictingChanges.get(), undefined); diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/scrollSynchronizer.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/scrollSynchronizer.ts index dec1af45d1b..d0398fb912d 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/scrollSynchronizer.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/scrollSynchronizer.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable } from '../../../../../base/common/lifecycle.js'; -import { derivedWithStore, IObservable } from '../../../../../base/common/observable.js'; +import { derived, IObservable } from '../../../../../base/common/observable.js'; import { CodeEditorWidget } from '../../../../../editor/browser/widget/codeEditor/codeEditorWidget.js'; import { ScrollType } from '../../../../../editor/common/editorCommon.js'; import { DocumentLineRangeMap } from '../model/mapping.js'; @@ -40,7 +40,7 @@ export class ScrollSynchronizer extends Disposable { ) { super(); - const s = derivedWithStore((reader, store) => { + const s = derived((reader) => { const baseView = this.baseView.read(reader); const editors = [this.input1View, this.input2View, this.inputResultView, baseView].filter(isDefined); @@ -73,7 +73,7 @@ export class ScrollSynchronizer extends Disposable { }; for (const editorView of editors) { - store.add(editorView.editor.onDidScrollChange(e => { + reader.store.add(editorView.editor.onDidScrollChange(e => { if (!this._isSyncing) { return; } diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts index 8417427fbfc..0b205d21448 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts @@ -22,11 +22,9 @@ import { InputCodeEditorView } from './editors/inputCodeEditorView.js'; import { ResultCodeEditorView } from './editors/resultCodeEditorView.js'; export class MergeEditorViewModel extends Disposable { - private readonly manuallySetActiveModifiedBaseRange = observableValue< - { range: ModifiedBaseRange | undefined; counter: number } - >(this, { range: undefined, counter: 0 }); + private readonly manuallySetActiveModifiedBaseRange; - private readonly attachedHistory = this._register(new AttachedHistory(this.model.resultTextModel)); + private readonly attachedHistory; constructor( public readonly model: MergeEditorModel, @@ -39,6 +37,108 @@ export class MergeEditorViewModel extends Disposable { @INotificationService private readonly notificationService: INotificationService, ) { super(); + this.manuallySetActiveModifiedBaseRange = observableValue< + { range: ModifiedBaseRange | undefined; counter: number } + >(this, { range: undefined, counter: 0 }); + this.attachedHistory = this._register(new AttachedHistory(this.model.resultTextModel)); + this.shouldUseAppendInsteadOfAccept = observableConfigValue( + 'mergeEditor.shouldUseAppendInsteadOfAccept', + false, + this.configurationService, + ); + this.counter = 0; + this.lastFocusedEditor = derivedObservableWithWritableCache< + { view: CodeEditorView | undefined; counter: number } + >(this, (reader, lastValue) => { + const editors = [ + this.inputCodeEditorView1, + this.inputCodeEditorView2, + this.resultCodeEditorView, + this.baseCodeEditorView.read(reader), + ]; + const view = editors.find((e) => e && e.isFocused.read(reader)); + return view ? { view, counter: this.counter++ } : lastValue || { view: undefined, counter: this.counter++ }; + }); + this.baseShowDiffAgainst = derived<1 | 2 | undefined>(this, reader => { + const lastFocusedEditor = this.lastFocusedEditor.read(reader); + if (lastFocusedEditor.view === this.inputCodeEditorView1) { + return 1; + } else if (lastFocusedEditor.view === this.inputCodeEditorView2) { + return 2; + } + return undefined; + }); + this.focusedEditorType = derived(this, reader => { + const lastFocusedEditor = this.lastFocusedEditor.read(reader); + + if (!lastFocusedEditor.view) { + return undefined; + } + + if (lastFocusedEditor.view === this.inputCodeEditorView1) { + return 'input1'; + } else if (lastFocusedEditor.view === this.inputCodeEditorView2) { + return 'input2'; + } else if (lastFocusedEditor.view === this.resultCodeEditorView) { + return 'result'; + } else if (lastFocusedEditor.view === this.baseCodeEditorView.read(reader)) { + return 'base'; + } + + return undefined; + }); + this.selectionInBase = derived(this, reader => { + const sourceEditor = this.lastFocusedEditor.read(reader).view; + if (!sourceEditor) { + return undefined; + } + const selections = sourceEditor.selection.read(reader) || []; + + const rangesInBase = selections.map((selection) => { + if (sourceEditor === this.inputCodeEditorView1) { + return this.model.translateInputRangeToBase(1, selection); + } else if (sourceEditor === this.inputCodeEditorView2) { + return this.model.translateInputRangeToBase(2, selection); + } else if (sourceEditor === this.resultCodeEditorView) { + return this.model.translateResultRangeToBase(selection); + } else if (sourceEditor === this.baseCodeEditorView.read(reader)) { + return selection; + } else { + return selection; + } + }); + + return { + rangesInBase, + sourceEditor + }; + }); + this.activeModifiedBaseRange = derived(this, + (reader) => { + /** @description activeModifiedBaseRange */ + const focusedEditor = this.lastFocusedEditor.read(reader); + const manualRange = this.manuallySetActiveModifiedBaseRange.read(reader); + if (manualRange.counter > focusedEditor.counter) { + return manualRange.range; + } + + if (!focusedEditor.view) { + return; + } + const cursorLineNumber = focusedEditor.view.cursorLineNumber.read(reader); + if (!cursorLineNumber) { + return undefined; + } + + const modifiedBaseRanges = this.model.modifiedBaseRanges.read(reader); + return modifiedBaseRanges.find((r) => { + const range = this.getRangeOfModifiedBaseRange(focusedEditor.view!, r, reader); + return range.isEmpty + ? range.startLineNumber === cursorLineNumber + : range.contains(cursorLineNumber); + }); + } + ); this._register(resultCodeEditorView.editor.onDidChangeModelContent(e => { if (this.model.isApplyingEditInResult || e.isRedoing || e.isUndoing) { @@ -86,85 +186,19 @@ export class MergeEditorViewModel extends Disposable { })); } - public readonly shouldUseAppendInsteadOfAccept = observableConfigValue( - 'mergeEditor.shouldUseAppendInsteadOfAccept', - false, - this.configurationService, - ); + public readonly shouldUseAppendInsteadOfAccept; - private counter = 0; - private readonly lastFocusedEditor = derivedObservableWithWritableCache< - { view: CodeEditorView | undefined; counter: number } - >(this, (reader, lastValue) => { - const editors = [ - this.inputCodeEditorView1, - this.inputCodeEditorView2, - this.resultCodeEditorView, - this.baseCodeEditorView.read(reader), - ]; - const view = editors.find((e) => e && e.isFocused.read(reader)); - return view ? { view, counter: this.counter++ } : lastValue || { view: undefined, counter: this.counter++ }; - }); + private counter; + private readonly lastFocusedEditor; - public readonly baseShowDiffAgainst = derived<1 | 2 | undefined>(this, reader => { - const lastFocusedEditor = this.lastFocusedEditor.read(reader); - if (lastFocusedEditor.view === this.inputCodeEditorView1) { - return 1; - } else if (lastFocusedEditor.view === this.inputCodeEditorView2) { - return 2; - } - return undefined; - }); + public readonly baseShowDiffAgainst; /** * Returns an observable that tracks which editor type is currently focused */ - public readonly focusedEditorType = derived(this, reader => { - const lastFocusedEditor = this.lastFocusedEditor.read(reader); + public readonly focusedEditorType; - if (!lastFocusedEditor.view) { - return undefined; - } - - if (lastFocusedEditor.view === this.inputCodeEditorView1) { - return 'input1'; - } else if (lastFocusedEditor.view === this.inputCodeEditorView2) { - return 'input2'; - } else if (lastFocusedEditor.view === this.resultCodeEditorView) { - return 'result'; - } else if (lastFocusedEditor.view === this.baseCodeEditorView.read(reader)) { - return 'base'; - } - - return undefined; - }); - - public readonly selectionInBase = derived(this, reader => { - const sourceEditor = this.lastFocusedEditor.read(reader).view; - if (!sourceEditor) { - return undefined; - } - const selections = sourceEditor.selection.read(reader) || []; - - const rangesInBase = selections.map((selection) => { - if (sourceEditor === this.inputCodeEditorView1) { - return this.model.translateInputRangeToBase(1, selection); - } else if (sourceEditor === this.inputCodeEditorView2) { - return this.model.translateInputRangeToBase(2, selection); - } else if (sourceEditor === this.resultCodeEditorView) { - return this.model.translateResultRangeToBase(selection); - } else if (sourceEditor === this.baseCodeEditorView.read(reader)) { - return selection; - } else { - return selection; - } - }); - - return { - rangesInBase, - sourceEditor - }; - }); + public readonly selectionInBase; private getRangeOfModifiedBaseRange(editor: CodeEditorView, modifiedBaseRange: ModifiedBaseRange, reader: IReader | undefined): MergeEditorLineRange { if (editor === this.resultCodeEditorView) { @@ -177,32 +211,7 @@ export class MergeEditorViewModel extends Disposable { } } - public readonly activeModifiedBaseRange = derived(this, - (reader) => { - /** @description activeModifiedBaseRange */ - const focusedEditor = this.lastFocusedEditor.read(reader); - const manualRange = this.manuallySetActiveModifiedBaseRange.read(reader); - if (manualRange.counter > focusedEditor.counter) { - return manualRange.range; - } - - if (!focusedEditor.view) { - return; - } - const cursorLineNumber = focusedEditor.view.cursorLineNumber.read(reader); - if (!cursorLineNumber) { - return undefined; - } - - const modifiedBaseRanges = this.model.modifiedBaseRanges.read(reader); - return modifiedBaseRanges.find((r) => { - const range = this.getRangeOfModifiedBaseRange(focusedEditor.view!, r, reader); - return range.isEmpty - ? range.startLineNumber === cursorLineNumber - : range.contains(cursorLineNumber); - }); - } - ); + public readonly activeModifiedBaseRange; public setActiveModifiedBaseRange(range: ModifiedBaseRange | undefined, tx: ITransaction): void { this.manuallySetActiveModifiedBaseRange.set({ range, counter: this.counter++ }, tx); @@ -315,11 +324,13 @@ export class MergeEditorViewModel extends Disposable { } class AttachedHistory extends Disposable { - private readonly attachedHistory: { element: IAttachedHistoryElement; altId: number }[] = []; - private previousAltId: number = this.model.getAlternativeVersionId(); + private readonly attachedHistory: { element: IAttachedHistoryElement; altId: number }[]; + private previousAltId: number; constructor(private readonly model: ITextModel) { super(); + this.attachedHistory = []; + this.previousAltId = this.model.getAlternativeVersionId(); this._register(model.onDidChangeContent((e) => { const currentAltId = model.getAlternativeVersionId(); diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/viewZones.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/viewZones.ts index 6590791eac2..06b768ff5a8 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/viewZones.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/viewZones.ts @@ -17,15 +17,19 @@ import { getAlignments } from './lineAlignment.js'; import { MergeEditorViewModel } from './viewModel.js'; export class ViewZoneComputer { - private readonly conflictActionsFactoryInput1 = new ConflictActionsFactory(this.input1Editor); - private readonly conflictActionsFactoryInput2 = new ConflictActionsFactory(this.input2Editor); - private readonly conflictActionsFactoryResult = new ConflictActionsFactory(this.resultEditor); + private readonly conflictActionsFactoryInput1; + private readonly conflictActionsFactoryInput2; + private readonly conflictActionsFactoryResult; constructor( private readonly input1Editor: ICodeEditor, private readonly input2Editor: ICodeEditor, private readonly resultEditor: ICodeEditor, - ) { } + ) { + this.conflictActionsFactoryInput1 = new ConflictActionsFactory(this.input1Editor); + this.conflictActionsFactoryInput2 = new ConflictActionsFactory(this.input2Editor); + this.conflictActionsFactoryResult = new ConflictActionsFactory(this.resultEditor); + } public computeViewZones( reader: IReader, diff --git a/src/vs/workbench/contrib/mergeEditor/test/browser/mapping.test.ts b/src/vs/workbench/contrib/mergeEditor/test/browser/mapping.test.ts index bef40313331..593f8638c46 100644 --- a/src/vs/workbench/contrib/mergeEditor/test/browser/mapping.test.ts +++ b/src/vs/workbench/contrib/mergeEditor/test/browser/mapping.test.ts @@ -7,7 +7,7 @@ import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; import { Position } from '../../../../../editor/common/core/position.js'; import { Range } from '../../../../../editor/common/core/range.js'; -import { TextLength } from '../../../../../editor/common/core/textLength.js'; +import { TextLength } from '../../../../../editor/common/core/text/textLength.js'; import { DocumentRangeMap, RangeMapping } from '../../browser/model/mapping.js'; suite('merge editor mapping', () => { diff --git a/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput.ts b/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput.ts index aa09311d401..9c3b0f6083c 100644 --- a/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput.ts +++ b/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput.ts @@ -74,7 +74,7 @@ export class MultiDiffEditorInput extends EditorInput implements ILanguageSuppor override get capabilities(): EditorInputCapabilities { return EditorInputCapabilities.Readonly; } override get typeId(): string { return MultiDiffEditorInput.ID; } - private _name: string = ''; + private _name: string; override getName(): string { return this._name; } override get editorId(): string { return DEFAULT_EDITOR_ASSOCIATION.id; } @@ -92,6 +92,51 @@ export class MultiDiffEditorInput extends EditorInput implements ILanguageSuppor @ITextFileService private readonly _textFileService: ITextFileService, ) { super(); + this._name = ''; + this._viewModel = new LazyStatefulPromise(async () => { + const model = await this._createModel(); + this._register(model); + const vm = new MultiDiffEditorViewModel(model, this._instantiationService); + this._register(vm); + await raceTimeout(vm.waitForDiffs(), 1000); + return vm; + }); + this._resolvedSource = new ObservableLazyPromise(async () => { + const source: IResolvedMultiDiffSource | undefined = this.initialResources + ? { resources: ValueWithChangeEvent.const(this.initialResources) } + : await this._multiDiffSourceResolverService.resolve(this.multiDiffSource); + return { + source, + resources: source ? observableFromValueWithChangeEvent(this, source.resources) : constObservable([]), + }; + }); + this.resources = derived(this, reader => this._resolvedSource.cachedPromiseResult.read(reader)?.data?.resources.read(reader)); + this.textFileServiceOnDidChange = new FastEventDispatcher( + this._textFileService.files.onDidChangeDirty, + item => item.resource.toString(), + uri => uri.toString() + ); + this._isDirtyObservables = mapObservableArrayCached(this, this.resources.map(r => r ?? []), res => { + const isModifiedDirty = res.modifiedUri ? isUriDirty(this.textFileServiceOnDidChange, this._textFileService, res.modifiedUri) : constObservable(false); + const isOriginalDirty = res.originalUri ? isUriDirty(this.textFileServiceOnDidChange, this._textFileService, res.originalUri) : constObservable(false); + return derived(reader => /** @description modifiedDirty||originalDirty */ isModifiedDirty.read(reader) || isOriginalDirty.read(reader)); + }, i => i.getKey()); + this._isDirtyObservable = derived(this, reader => this._isDirtyObservables.read(reader).some(isDirty => isDirty.read(reader))) + .keepObserved(this._store); + this.onDidChangeDirty = Event.fromObservableLight(this._isDirtyObservable); + this.closeHandler = { + + // This is a workaround for not having a better way + // to figure out if the editors this input wraps + // around are opened or not + + async confirm() { + return ConfirmResult.DONT_SAVE; + }, + showConfirm() { + return false; + } + }; this._register(autorun((reader) => { /** @description Updates name */ @@ -133,14 +178,7 @@ export class MultiDiffEditorInput extends EditorInput implements ILanguageSuppor return this._viewModel.getPromise(); } - private readonly _viewModel = new LazyStatefulPromise(async () => { - const model = await this._createModel(); - this._register(model); - const vm = new MultiDiffEditorViewModel(model, this._instantiationService); - this._register(vm); - await raceTimeout(vm.waitForDiffs(), 1000); - return vm; - }); + private readonly _viewModel; private async _createModel(): Promise { const source = await this._resolvedSource.getPromise(); @@ -209,15 +247,7 @@ export class MultiDiffEditorInput extends EditorInput implements ILanguageSuppor return result; } - private readonly _resolvedSource = new ObservableLazyPromise(async () => { - const source: IResolvedMultiDiffSource | undefined = this.initialResources - ? { resources: ValueWithChangeEvent.const(this.initialResources) } - : await this._multiDiffSourceResolverService.resolve(this.multiDiffSource); - return { - source, - resources: source ? observableFromValueWithChangeEvent(this, source.resources) : constObservable([]), - }; - }); + private readonly _resolvedSource; override matches(otherInput: EditorInput | IUntypedEditorInput): boolean { if (super.matches(otherInput)) { @@ -231,23 +261,14 @@ export class MultiDiffEditorInput extends EditorInput implements ILanguageSuppor return false; } - public readonly resources = derived(this, reader => this._resolvedSource.cachedPromiseResult.read(reader)?.data?.resources.read(reader)); + public readonly resources; - private readonly textFileServiceOnDidChange = new FastEventDispatcher( - this._textFileService.files.onDidChangeDirty, - item => item.resource.toString(), - uri => uri.toString() - ); + private readonly textFileServiceOnDidChange; - private readonly _isDirtyObservables = mapObservableArrayCached(this, this.resources.map(r => r ?? []), res => { - const isModifiedDirty = res.modifiedUri ? isUriDirty(this.textFileServiceOnDidChange, this._textFileService, res.modifiedUri) : constObservable(false); - const isOriginalDirty = res.originalUri ? isUriDirty(this.textFileServiceOnDidChange, this._textFileService, res.originalUri) : constObservable(false); - return derived(reader => /** @description modifiedDirty||originalDirty */ isModifiedDirty.read(reader) || isOriginalDirty.read(reader)); - }, i => i.getKey()); - private readonly _isDirtyObservable = derived(this, reader => this._isDirtyObservables.read(reader).some(isDirty => isDirty.read(reader))) - .keepObserved(this._store); + private readonly _isDirtyObservables; + private readonly _isDirtyObservable; - override readonly onDidChangeDirty = Event.fromObservableLight(this._isDirtyObservable); + override readonly onDidChangeDirty; override isDirty() { return this._isDirtyObservable.get(); } override async save(group: number, options?: ISaveOptions | undefined): Promise { @@ -277,19 +298,7 @@ export class MultiDiffEditorInput extends EditorInput implements ILanguageSuppor return undefined; } - override readonly closeHandler: IEditorCloseHandler = { - - // This is a workaround for not having a better way - // to figure out if the editors this input wraps - // around are opened or not - - async confirm() { - return ConfirmResult.DONT_SAVE; - }, - showConfirm() { - return false; - } - }; + override readonly closeHandler: IEditorCloseHandler; } export interface IDocumentDiffItemWithMultiDiffEditorItem extends IDocumentDiffItem { diff --git a/src/vs/workbench/contrib/multiDiffEditor/browser/scmMultiDiffSourceResolver.ts b/src/vs/workbench/contrib/multiDiffEditor/browser/scmMultiDiffSourceResolver.ts index ec198f8ad63..73903ffaf42 100644 --- a/src/vs/workbench/contrib/multiDiffEditor/browser/scmMultiDiffSourceResolver.ts +++ b/src/vs/workbench/contrib/multiDiffEditor/browser/scmMultiDiffSourceResolver.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { ValueWithChangeEvent } from '../../../../base/common/event.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { observableFromEvent, ValueWithChangeEventFromObservable, waitForState } from '../../../../base/common/observable.js'; import { URI, UriComponents } from '../../../../base/common/uri.js'; @@ -13,6 +14,7 @@ import { ContextKeyValue } from '../../../../platform/contextkey/common/contextk import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { IActivityService, ProgressBadge } from '../../../services/activity/common/activity.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; +import { ISCMHistoryItem } from '../../scm/common/history.js'; import { ISCMRepository, ISCMResourceGroup, ISCMService } from '../../scm/common/scm.js'; import { IMultiDiffSourceResolver, IMultiDiffSourceResolverService, IResolvedMultiDiffSource, MultiDiffEditorItem } from './multiDiffSourceResolverService.js'; @@ -83,22 +85,93 @@ export class ScmMultiDiffSourceResolver implements IMultiDiffSourceResolver { } } -class ScmResolvedMultiDiffSource implements IResolvedMultiDiffSource { - private readonly _resources = observableFromEvent( - this._group.onDidChangeResources, - () => /** @description resources */ this._group.resources.map(e => new MultiDiffEditorItem(e.multiDiffEditorOriginalUri, e.multiDiffEditorModifiedUri, e.sourceUri)) - ); - readonly resources = new ValueWithChangeEventFromObservable(this._resources); +interface ScmHistoryItemUriFields { + readonly repositoryId: string; + readonly historyItemId: string; + readonly historyItemParentId?: string; +} - public readonly contextKeys: Record = { - scmResourceGroup: this._group.id, - scmProvider: this._repository.provider.contextValue, - }; +export class ScmHistoryItemResolver implements IMultiDiffSourceResolver { + private static readonly _scheme = 'scm-history-item'; + + public static getMultiDiffSourceUri(repositoryId: string, historyItem: ISCMHistoryItem): URI { + const historyItemParentId = historyItem.parentIds.length > 0 ? historyItem.parentIds[0] : undefined; + + return URI.from({ + scheme: ScmHistoryItemResolver._scheme, + query: JSON.stringify({ + repositoryId, + historyItemId: historyItem.id, + historyItemParentId + } satisfies ScmHistoryItemUriFields) + }, true); + } + + constructor(@ISCMService private readonly _scmService: ISCMService) { } + + canHandleUri(uri: URI): boolean { + return this._parseUri(uri) !== undefined; + } + + async resolveDiffSource(uri: URI): Promise { + const { repositoryId, historyItemId, historyItemParentId } = this._parseUri(uri)!; + + const repository = this._scmService.getRepository(repositoryId); + const historyProvider = repository?.provider.historyProvider.get(); + const historyItemChanges = await historyProvider?.provideHistoryItemChanges(historyItemId, historyItemParentId) ?? []; + + const resources = ValueWithChangeEvent.const( + historyItemChanges.map(change => new MultiDiffEditorItem(change.originalUri, change.modifiedUri, change.uri))); + + return { resources }; + } + + private _parseUri(uri: URI): ScmHistoryItemUriFields | undefined { + if (uri.scheme !== ScmHistoryItemResolver._scheme) { + return undefined; + } + + let query: ScmHistoryItemUriFields; + try { + query = JSON.parse(uri.query) as ScmHistoryItemUriFields; + } catch (e) { + return undefined; + } + + if (typeof query !== 'object' || query === null) { + return undefined; + } + + const { repositoryId, historyItemId, historyItemParentId } = query; + if (typeof repositoryId !== 'string' || typeof historyItemId !== 'string' || + (typeof historyItemParentId !== 'string' && historyItemParentId !== undefined)) { + return undefined; + } + + return { repositoryId, historyItemId, historyItemParentId }; + } +} + +class ScmResolvedMultiDiffSource implements IResolvedMultiDiffSource { + private readonly _resources; + readonly resources; + + public readonly contextKeys: Record; constructor( private readonly _group: ISCMResourceGroup, private readonly _repository: ISCMRepository, - ) { } + ) { + this._resources = observableFromEvent( + this._group.onDidChangeResources, + () => /** @description resources */ this._group.resources.map(e => new MultiDiffEditorItem(e.multiDiffEditorOriginalUri, e.multiDiffEditorModifiedUri, e.sourceUri)) + ); + this.resources = new ValueWithChangeEventFromObservable(this._resources); + this.contextKeys = { + scmResourceGroup: this._group.id, + scmProvider: this._repository.provider.contextValue, + }; + } } interface UriFields { @@ -116,6 +189,7 @@ export class ScmMultiDiffSourceResolverContribution extends Disposable { ) { super(); + this._register(multiDiffSourceResolverService.registerResolver(instantiationService.createInstance(ScmHistoryItemResolver))); this._register(multiDiffSourceResolverService.registerResolver(instantiationService.createInstance(ScmMultiDiffSourceResolver))); } } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/multicursor/notebookMulticursor.ts b/src/vs/workbench/contrib/notebook/browser/contrib/multicursor/notebookMulticursor.ts index 116d5d3fab9..9a0390c9a41 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/multicursor/notebookMulticursor.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/multicursor/notebookMulticursor.ts @@ -90,28 +90,28 @@ export class NotebookMultiCursorController extends Disposable implements INotebo static readonly id: string = 'notebook.multiCursorController'; - private word: string = ''; + private word: string; private startPosition: { cellIndex: number; position: Position; } | undefined; - private trackedCells: TrackedCell[] = []; + private trackedCells: TrackedCell[]; - private readonly _onDidChangeAnchorCell = this._register(new Emitter()); - readonly onDidChangeAnchorCell: Event = this._onDidChangeAnchorCell.event; + private readonly _onDidChangeAnchorCell; + readonly onDidChangeAnchorCell: Event; private anchorCell: [ICellViewModel, ICodeEditor] | undefined; - private readonly anchorDisposables = this._register(new DisposableStore()); - private readonly cursorsDisposables = this._register(new DisposableStore()); - private cursorsControllers: ResourceMap = new ResourceMap(); + private readonly anchorDisposables; + private readonly cursorsDisposables; + private cursorsControllers: ResourceMap; - private state: NotebookMultiCursorState = NotebookMultiCursorState.Idle; + private state: NotebookMultiCursorState; public getState(): NotebookMultiCursorState { return this.state; } - private _nbIsMultiSelectSession = NOTEBOOK_MULTI_CURSOR_CONTEXT.IsNotebookMultiCursor.bindTo(this.contextKeyService); - private _nbMultiSelectState = NOTEBOOK_MULTI_CURSOR_CONTEXT.NotebookMultiSelectCursorState.bindTo(this.contextKeyService); + private _nbIsMultiSelectSession; + private _nbMultiSelectState; constructor( private readonly notebookEditor: INotebookEditor, @@ -123,6 +123,16 @@ export class NotebookMultiCursorController extends Disposable implements INotebo @IUndoRedoService private readonly undoRedoService: IUndoRedoService, ) { super(); + this.word = ''; + this.trackedCells = []; + this._onDidChangeAnchorCell = this._register(new Emitter()); + this.onDidChangeAnchorCell = this._onDidChangeAnchorCell.event; + this.anchorDisposables = this._register(new DisposableStore()); + this.cursorsDisposables = this._register(new DisposableStore()); + this.cursorsControllers = new ResourceMap(); + this.state = NotebookMultiCursorState.Idle; + this._nbIsMultiSelectSession = NOTEBOOK_MULTI_CURSOR_CONTEXT.IsNotebookMultiCursor.bindTo(this.contextKeyService); + this._nbMultiSelectState = NOTEBOOK_MULTI_CURSOR_CONTEXT.NotebookMultiSelectCursorState.bindTo(this.contextKeyService); this.anchorCell = this.notebookEditor.activeCellAndCodeEditor; diff --git a/src/vs/workbench/contrib/notebook/browser/controller/chat/notebook.chat.contribution.ts b/src/vs/workbench/contrib/notebook/browser/controller/chat/notebook.chat.contribution.ts index 6993def3c8a..b1f977c9b33 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/chat/notebook.chat.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/chat/notebook.chat.contribution.ts @@ -16,7 +16,7 @@ import { localize } from '../../../../../../nls.js'; import { Action2, MenuId, registerAction2 } from '../../../../../../platform/actions/common/actions.js'; import { ContextKeyExpr, IContextKey, IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; import { ServicesAccessor } from '../../../../../../platform/instantiation/common/instantiation.js'; -import { IQuickInputService, IQuickPickItem } from '../../../../../../platform/quickinput/common/quickInput.js'; +import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../../../platform/quickinput/common/quickInput.js'; import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../../common/contributions.js'; import { IEditorService } from '../../../../../services/editor/common/editorService.js'; import { IChatWidget, IChatWidgetService, showChatView } from '../../../../chat/browser/chat.js'; @@ -37,6 +37,8 @@ import './cellChatActions.js'; import { CTX_NOTEBOOK_CHAT_HAS_AGENT } from './notebookChatContext.js'; import { IViewsService } from '../../../../../services/views/common/viewsService.js'; import { createNotebookOutputVariableEntry, NOTEBOOK_CELL_OUTPUT_MIME_TYPE_LIST_FOR_CHAT_CONST } from '../../contrib/chat/notebookChatUtils.js'; +import { IChatContextPickerItem, IChatContextPickerPickItem, IChatContextPickService } from '../../../../chat/browser/chatContextPickService.js'; +import { Codicon } from '../../../../../../base/common/codicons.js'; const NotebookKernelVariableKey = 'kernelVariable'; @@ -52,9 +54,12 @@ class NotebookChatContribution extends Disposable implements IWorkbenchContribut @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, @INotebookKernelService private readonly notebookKernelService: INotebookKernelService, @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, + @IChatContextPickService chatContextPickService: IChatContextPickService ) { super(); + this._register(chatContextPickService.registerChatContextItem(new KernelVariableContextPicker(this.editorService, this.notebookKernelService))); + this._ctxHasProvider = CTX_NOTEBOOK_CHAT_HAS_AGENT.bindTo(contextKeyService); const updateNotebookAgentStatus = () => { @@ -201,7 +206,7 @@ export class SelectAndInsertKernelVariableAction extends Action2 { }); } - const pickedVariable = await quickInputService.pick(quickPickItems, { placeHolder: 'Select a kernel variable' }); + const pickedVariable = await quickInputService.pick(quickPickItems, { placeHolder: localize('selectKernelVariablePlaceholder', "Select a kernel variable") }); if (!pickedVariable) { return; } @@ -240,6 +245,67 @@ export class SelectAndInsertKernelVariableAction extends Action2 { } } +class KernelVariableContextPicker implements IChatContextPickerItem { + + readonly type = 'pickerPick'; + readonly label = localize('chatContext.notebook.kernelVariable', 'Kernel Variable...'); + readonly icon = Codicon.serverEnvironment; + + constructor( + @IEditorService private readonly editorService: IEditorService, + @INotebookKernelService private readonly notebookKernelService: INotebookKernelService, + ) { } + + isEnabled(widget: IChatWidget): Promise | boolean { + return widget.location === ChatAgentLocation.Notebook && Boolean(getNotebookEditorFromEditorPane(this.editorService.activeEditorPane)?.getViewModel()?.notebookDocument); + } + + asPicker(): { readonly placeholder: string; readonly picks: Promise<(IChatContextPickerPickItem | IQuickPickSeparator)[]> } { + + const picks = (async () => { + + const notebook = getNotebookEditorFromEditorPane(this.editorService.activeEditorPane)?.getViewModel()?.notebookDocument; + + if (!notebook) { + return []; + } + + const selectedKernel = this.notebookKernelService.getMatchingKernel(notebook).selected; + const hasVariableProvider = selectedKernel?.hasVariableProvider; + + if (!hasVariableProvider) { + return []; + } + + const variables = selectedKernel.provideVariables(notebook.uri, undefined, 'named', 0, CancellationToken.None); + + const result: IChatContextPickerPickItem[] = []; + for await (const variable of variables) { + result.push({ + label: variable.name, + description: variable.value, + asAttachment: () => { + return { + kind: 'generic', + id: 'vscode.notebook.variable', + name: variable.name, + value: variable.value, + icon: codiconsLibrary.variable, + }; + }, + }); + } + + return result; + })(); + + return { + placeholder: localize('chatContext.notebook.kernelVariable.placeholder', 'Select a kernel variable'), + picks + }; + } +} + registerAction2(class CopyCellOutputAction extends Action2 { constructor() { diff --git a/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts index 3ff16d064f3..cd153dbe702 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts @@ -680,8 +680,8 @@ export class SideBySideDiffElementViewModel extends DiffElementCellViewModelBase return this.mainDocumentTextModel; } - override readonly original!: DiffNestedCellViewModel; - override readonly modified!: DiffNestedCellViewModel; + declare readonly original: DiffNestedCellViewModel; + declare readonly modified: DiffNestedCellViewModel; override readonly type: 'unchanged' | 'modified'; /** diff --git a/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts index 201991905d5..7d7136f41e3 100644 --- a/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts @@ -524,8 +524,8 @@ export class NotebookService extends Disposable implements INotebookService { private readonly _memento: Memento; private readonly _viewTypeCache: MementoObject; - private readonly _notebookProviders = new Map(); - private _notebookProviderInfoStore: NotebookProviderInfoStore | undefined = undefined; + private readonly _notebookProviders; + private _notebookProviderInfoStore: NotebookProviderInfoStore | undefined; private get notebookProviderInfoStore(): NotebookProviderInfoStore { if (!this._notebookProviderInfoStore) { this._notebookProviderInfoStore = this._register(this._instantiationService.createInstance(NotebookProviderInfoStore)); @@ -533,35 +533,35 @@ export class NotebookService extends Disposable implements INotebookService { return this._notebookProviderInfoStore; } - private readonly _notebookRenderersInfoStore = this._instantiationService.createInstance(NotebookOutputRendererInfoStore); - private readonly _onDidChangeOutputRenderers = this._register(new Emitter()); - readonly onDidChangeOutputRenderers = this._onDidChangeOutputRenderers.event; + private readonly _notebookRenderersInfoStore; + private readonly _onDidChangeOutputRenderers; + readonly onDidChangeOutputRenderers; - private readonly _notebookStaticPreloadInfoStore = new Set(); + private readonly _notebookStaticPreloadInfoStore; - private readonly _models = new ResourceMap(); + private readonly _models; - private readonly _onWillAddNotebookDocument = this._register(new Emitter()); - private readonly _onDidAddNotebookDocument = this._register(new Emitter()); - private readonly _onWillRemoveNotebookDocument = this._register(new Emitter()); - private readonly _onDidRemoveNotebookDocument = this._register(new Emitter()); + private readonly _onWillAddNotebookDocument; + private readonly _onDidAddNotebookDocument; + private readonly _onWillRemoveNotebookDocument; + private readonly _onDidRemoveNotebookDocument; - readonly onWillAddNotebookDocument = this._onWillAddNotebookDocument.event; - readonly onDidAddNotebookDocument = this._onDidAddNotebookDocument.event; - readonly onDidRemoveNotebookDocument = this._onDidRemoveNotebookDocument.event; - readonly onWillRemoveNotebookDocument = this._onWillRemoveNotebookDocument.event; + readonly onWillAddNotebookDocument; + readonly onDidAddNotebookDocument; + readonly onDidRemoveNotebookDocument; + readonly onWillRemoveNotebookDocument; - private readonly _onAddViewType = this._register(new Emitter()); - readonly onAddViewType = this._onAddViewType.event; + private readonly _onAddViewType; + readonly onAddViewType; - private readonly _onWillRemoveViewType = this._register(new Emitter()); - readonly onWillRemoveViewType = this._onWillRemoveViewType.event; + private readonly _onWillRemoveViewType; + readonly onWillRemoveViewType; - private readonly _onDidChangeEditorTypes = this._register(new Emitter()); - onDidChangeEditorTypes: Event = this._onDidChangeEditorTypes.event; + private readonly _onDidChangeEditorTypes; + onDidChangeEditorTypes: Event; private _cutItems: NotebookCellTextModel[] | undefined; - private _lastClipboardIsCopy: boolean = true; + private _lastClipboardIsCopy: boolean; private _displayOrder!: MimeTypeDisplayOrder; @@ -574,6 +574,28 @@ export class NotebookService extends Disposable implements INotebookService { @INotebookDocumentService private readonly _notebookDocumentService: INotebookDocumentService ) { super(); + this._notebookProviders = new Map(); + this._notebookProviderInfoStore = undefined; + this._notebookRenderersInfoStore = this._instantiationService.createInstance(NotebookOutputRendererInfoStore); + this._onDidChangeOutputRenderers = this._register(new Emitter()); + this.onDidChangeOutputRenderers = this._onDidChangeOutputRenderers.event; + this._notebookStaticPreloadInfoStore = new Set(); + this._models = new ResourceMap(); + this._onWillAddNotebookDocument = this._register(new Emitter()); + this._onDidAddNotebookDocument = this._register(new Emitter()); + this._onWillRemoveNotebookDocument = this._register(new Emitter()); + this._onDidRemoveNotebookDocument = this._register(new Emitter()); + this.onWillAddNotebookDocument = this._onWillAddNotebookDocument.event; + this.onDidAddNotebookDocument = this._onDidAddNotebookDocument.event; + this.onDidRemoveNotebookDocument = this._onDidRemoveNotebookDocument.event; + this.onWillRemoveNotebookDocument = this._onWillRemoveNotebookDocument.event; + this._onAddViewType = this._register(new Emitter()); + this.onAddViewType = this._onAddViewType.event; + this._onWillRemoveViewType = this._register(new Emitter()); + this.onWillRemoveViewType = this._onWillRemoveViewType.event; + this._onDidChangeEditorTypes = this._register(new Emitter()); + this.onDidChangeEditorTypes = this._onDidChangeEditorTypes.event; + this._lastClipboardIsCopy = true; notebookRendererExtensionPoint.setHandler((renderers) => { this._notebookRenderersInfoStore.clear(); diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts index 508e9f18e06..0c7a1eb0bb3 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts @@ -78,7 +78,7 @@ function validateWebviewBoundary(element: HTMLElement) { } export class NotebookCellList extends WorkbenchList implements IDisposable, IStyleController, INotebookCellList { - protected override readonly view!: NotebookCellListView; + declare protected readonly view: NotebookCellListView; private viewZones!: NotebookViewZones; private cellOverlays!: NotebookCellOverlays; get onWillScroll(): Event { return this.view.onWillScroll; } diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts index 26f94306030..f042035c5d4 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts @@ -1070,7 +1070,7 @@ async function webviewPreloads(ctx: PreloadContext) { }, blob(): Blob { - return new Blob([valueBytes], { type: this.mime }); + return new Blob([valueBytes as Uint8Array], { type: this.mime }); }, get _allOutputItems() { @@ -2520,7 +2520,7 @@ async function webviewPreloads(ctx: PreloadContext) { }, blob(): Blob { - return new Blob([this.data()], { type: this.mime }); + return new Blob([this.data() as Uint8Array], { type: this.mime }); }, _allOutputItems: [{ diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorToolbar.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorToolbar.ts index 8c02d7def9f..f1c80959c87 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorToolbar.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorToolbar.ts @@ -185,7 +185,7 @@ class WorkbenchDynamicLabelStrategy implements IActionLayoutStrategy { return undefined; } else { if (action instanceof MenuItemAction) { - this.instantiationService.createInstance(MenuEntryActionViewItem, action, { hoverDelegate: options.hoverDelegate }); + return this.instantiationService.createInstance(MenuEntryActionViewItem, action, { hoverDelegate: options.hoverDelegate }); } if (action instanceof SubmenuItemAction) { @@ -372,9 +372,6 @@ export class NotebookEditorWorkbenchToolbar extends Disposable { this._notebookTopLeftToolbarContainer, leftToolbarOptions ); - - - this._register(this._notebookLeftToolbar); this._notebookLeftToolbar.context = context; diff --git a/src/vs/workbench/contrib/preferences/browser/media/preferencesEditor.css b/src/vs/workbench/contrib/preferences/browser/media/preferencesEditor.css new file mode 100644 index 00000000000..d767ca89ba5 --- /dev/null +++ b/src/vs/workbench/contrib/preferences/browser/media/preferencesEditor.css @@ -0,0 +1,104 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.preferences-editor { + + height: 100%; + overflow: hidden; + max-width: 1200px; + margin: auto; + + .preferences-editor-header { + box-sizing: border-box; + margin: auto; + overflow: hidden; + margin-top: 11px; + padding-top: 3px; + padding-left: 24px; + padding-right: 24px; + max-width: 1200px; + + .search-container { + position: relative; + + .suggest-input-container { + border: 1px solid #ddd; + } + } + + .preferences-tabs-container { + height: 32px; + display: flex; + border-bottom: solid 1px; + margin-top: 10px; + border-color: var(--vscode-settings-headerBorder); + + .action-item { + max-width: 300px; + overflow: hidden; + text-overflow: ellipsis; + + .action-title { + text-overflow: ellipsis; + overflow: hidden; + } + + .action-details { + opacity: 0.9; + text-transform: none; + margin-left: 0.5em; + font-size: 10px; + } + + .action-label { + font-size: 13px; + padding: 7px 8px 6.5px 8px; + opacity: 0.9; + border-radius: 0; + color: var(--vscode-foreground); + overflow: hidden; + text-overflow: ellipsis; + background: none !important; + color: var(--vscode-panelTitle-inactiveForeground); + } + + .action-label.checked { + opacity: 1; + color: var(--vscode-settings-headerForeground); + border-bottom: 1px solid var(--vscode-panelTitle-activeBorder); + outline: 1px solid var(--vscode-contrastActiveBorder, transparent); + outline-offset: -1px; + } + + .action-label:hover { + color: var(--vscode-panelTitle-activeForeground); + border-bottom: 1px solid var(--vscode-panelTitle-activeBorder); + outline: 1px solid var(--vscode-contrastActiveBorder, transparent); + outline-offset: -1px; + } + + .action-label:focus { + border-bottom: 1px solid var(--vscode-focusBorder); + outline: 1px solid transparent; + outline-offset: -1px; + } + + .action-label.checked:not(:focus) { + border-bottom-color: var(--vscode-settings-headerForeground); + } + + .action-label:not(.checked):not(:focus) { + /* Still maintain a border for alignment, but keep it transparent */ + border-bottom: 1px solid transparent; + } + + .action-label:not(.checked):hover { + outline-style: dashed; + } + } + + } + } +} diff --git a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts index f1a4ac3ff04..7ad89bcc89f 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts @@ -39,7 +39,7 @@ import { IWorkbenchEnvironmentService } from '../../../services/environment/comm import { IExtensionService } from '../../../services/extensions/common/extensions.js'; import { KeybindingsEditorInput } from '../../../services/preferences/browser/keybindingsEditorInput.js'; import { DEFINE_KEYBINDING_EDITOR_CONTRIB_ID, IDefineKeybindingEditorContribution, IPreferencesService } from '../../../services/preferences/common/preferences.js'; -import { SettingsEditor2Input } from '../../../services/preferences/common/preferencesEditorInput.js'; +import { PreferencesEditorInput, SettingsEditor2Input } from '../../../services/preferences/common/preferencesEditorInput.js'; import { IUserDataProfileService, CURRENT_PROFILE_CONTEXT } from '../../../services/userDataProfile/common/userDataProfile.js'; import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js'; import { ICodeEditor, isCodeEditor } from '../../../../editor/browser/editorBrowser.js'; @@ -50,6 +50,7 @@ import { IListService } from '../../../../platform/list/browser/listService.js'; import { SettingsEditorModel } from '../../../services/preferences/common/preferencesModels.js'; import { IPreferencesRenderer, WorkspaceSettingsRenderer, UserSettingsRenderer } from './preferencesRenderers.js'; import { ConfigurationTarget } from '../../../../platform/configuration/common/configuration.js'; +import { PreferencesEditor } from './preferencesEditor.js'; const SETTINGS_EDITOR_COMMAND_SEARCH = 'settings.action.search'; @@ -78,6 +79,32 @@ Registry.as(EditorExtensions.EditorPane).registerEditorPane ] ); +Registry.as(EditorExtensions.EditorPane).registerEditorPane( + EditorPaneDescriptor.create( + PreferencesEditor, + PreferencesEditor.ID, + nls.localize('preferencesEditor', "Preferences Editor") + ), + [ + new SyncDescriptor(PreferencesEditorInput) + ] +); + +class PreferencesEditorInputSerializer implements IEditorSerializer { + + canSerialize(editorInput: EditorInput): boolean { + return true; + } + + serialize(editorInput: EditorInput): string { + return ''; + } + + deserialize(instantiationService: IInstantiationService): EditorInput { + return instantiationService.createInstance(PreferencesEditorInput); + } +} + Registry.as(EditorExtensions.EditorPane).registerEditorPane( EditorPaneDescriptor.create( KeybindingsEditor, @@ -119,6 +146,7 @@ class SettingsEditor2InputSerializer implements IEditorSerializer { } } +Registry.as(EditorExtensions.EditorFactory).registerEditorSerializer(PreferencesEditorInput.ID, PreferencesEditorInputSerializer); Registry.as(EditorExtensions.EditorFactory).registerEditorSerializer(KeybindingsEditorInput.ID, KeybindingsEditorInputSerializer); Registry.as(EditorExtensions.EditorFactory).registerEditorSerializer(SettingsEditor2Input.ID, SettingsEditor2InputSerializer); diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesEditor.ts b/src/vs/workbench/contrib/preferences/browser/preferencesEditor.ts new file mode 100644 index 00000000000..9d2437c263f --- /dev/null +++ b/src/vs/workbench/contrib/preferences/browser/preferencesEditor.ts @@ -0,0 +1,174 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import './media/preferencesEditor.css'; +import * as DOM from '../../../../base/browser/dom.js'; +import { localize } from '../../../../nls.js'; +import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { IStorageService } from '../../../../platform/storage/common/storage.js'; +import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; +import { Event } from '../../../../base/common/event.js'; +import { getInputBoxStyle } from '../../../../platform/theme/browser/defaultStyles.js'; +import { IThemeService } from '../../../../platform/theme/common/themeService.js'; +import { EditorPane } from '../../../browser/parts/editor/editorPane.js'; +import { IEditorGroup } from '../../../services/editor/common/editorGroupsService.js'; +import { CONTEXT_PREFERENCES_SEARCH_FOCUS } from '../common/preferences.js'; +import { settingsTextInputBorder } from '../common/settingsEditorColorRegistry.js'; +import { SearchWidget } from './preferencesWidgets.js'; +import { ActionBar, ActionsOrientation } from '../../../../base/browser/ui/actionbar/actionbar.js'; +import { Registry } from '../../../../platform/registry/common/platform.js'; +import { IPreferencesEditorPaneRegistry, Extensions, IPreferencesEditorPaneDescriptor, IPreferencesEditorPane } from './preferencesEditorRegistry.js'; +import { Action } from '../../../../base/common/actions.js'; +import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { IEditorOptions } from '../../../../platform/editor/common/editor.js'; +import { IEditorOpenContext } from '../../../common/editor.js'; +import { EditorInput } from '../../../common/editor/editorInput.js'; +import { MutableDisposable } from '../../../../base/common/lifecycle.js'; + +class PreferenceTabAction extends Action { + constructor(readonly descriptor: IPreferencesEditorPaneDescriptor, actionCallback: () => void) { + super(descriptor.id, descriptor.title, '', true, actionCallback); + } +} + +export class PreferencesEditor extends EditorPane { + + static readonly ID: string = 'workbench.editor.preferences'; + + private readonly editorPanesRegistry = Registry.as(Extensions.PreferencesEditorPane); + + private readonly element: HTMLElement; + private readonly bodyElement: HTMLElement; + private readonly searchWidget: SearchWidget; + private readonly preferencesTabActionBar: ActionBar; + private readonly preferencesTabActions: PreferenceTabAction[] = []; + private readonly preferencesEditorPane = this._register(new MutableDisposable()); + + private readonly searchFocusContextKey: IContextKey; + + private dimension: DOM.Dimension | undefined; + + constructor( + group: IEditorGroup, + @ITelemetryService telemetryService: ITelemetryService, + @IThemeService themeService: IThemeService, + @IStorageService storageService: IStorageService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IContextKeyService contextKeyService: IContextKeyService, + ) { + super(PreferencesEditor.ID, group, telemetryService, themeService, storageService); + + this.searchFocusContextKey = CONTEXT_PREFERENCES_SEARCH_FOCUS.bindTo(contextKeyService); + + this.element = DOM.$('.preferences-editor'); + const headerContainer = DOM.append(this.element, DOM.$('.preferences-editor-header')); + + const searchContainer = DOM.append(headerContainer, DOM.$('.search-container')); + this.searchWidget = this._register(this.instantiationService.createInstance(SearchWidget, searchContainer, { + focusKey: this.searchFocusContextKey, + inputBoxStyles: getInputBoxStyle({ + inputBorder: settingsTextInputBorder + }) + })); + this._register(Event.debounce(this.searchWidget.onDidChange, () => undefined, 300)(() => { + this.preferencesEditorPane.value?.search(this.searchWidget.getValue()); + })); + + const preferencesTabsContainer = DOM.append(headerContainer, DOM.$('.preferences-tabs-container')); + this.preferencesTabActionBar = this._register(new ActionBar(preferencesTabsContainer, { + orientation: ActionsOrientation.HORIZONTAL, + focusOnlyEnabledItems: true, + ariaLabel: localize('preferencesTabSwitcherBarAriaLabel', "Preferences Tab Switcher"), + ariaRole: 'tablist', + })); + this.onDidChangePreferencesEditorPane(this.editorPanesRegistry.getPreferencesEditorPanes(), []); + this._register(this.editorPanesRegistry.onDidRegisterPreferencesEditorPanes(descriptors => this.onDidChangePreferencesEditorPane(descriptors, []))); + this._register(this.editorPanesRegistry.onDidDeregisterPreferencesEditorPanes(descriptors => this.onDidChangePreferencesEditorPane([], descriptors))); + + this.bodyElement = DOM.append(this.element, DOM.$('.preferences-editor-body')); + } + + protected createEditor(parent: HTMLElement): void { + DOM.append(parent, this.element); + } + + layout(dimension: DOM.Dimension): void { + this.dimension = dimension; + this.searchWidget.layout(dimension); + this.searchWidget.inputBox.inputElement.style.paddingRight = `12px`; + + this.preferencesEditorPane.value?.layout(new DOM.Dimension(this.bodyElement.clientWidth, dimension.height - 87 /* header height */)); + } + + override async setInput(input: EditorInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + await super.setInput(input, options, context, token); + if (this.preferencesTabActions.length) { + this.onDidSelectPreferencesEditorPane(this.preferencesTabActions[0].id); + } + } + + private onDidChangePreferencesEditorPane(toAdd: readonly IPreferencesEditorPaneDescriptor[], toRemove: readonly IPreferencesEditorPaneDescriptor[]): void { + for (const desc of toRemove) { + const index = this.preferencesTabActions.findIndex(action => action.id === desc.id); + if (index !== -1) { + this.preferencesTabActionBar.pull(index); + this.preferencesTabActions[index].dispose(); + this.preferencesTabActions.splice(index, 1); + } + } + if (toAdd.length > 0) { + const all = this.editorPanesRegistry.getPreferencesEditorPanes(); + for (const desc of toAdd) { + const index = all.findIndex(action => action.id === desc.id); + if (index !== -1) { + const action = new PreferenceTabAction(desc, () => this.onDidSelectPreferencesEditorPane(desc.id)); + this.preferencesTabActions.splice(index, 0, action); + this.preferencesTabActionBar.push(action, { index }); + } + } + } + } + + private onDidSelectPreferencesEditorPane(id: string): void { + let selectedAction: PreferenceTabAction | undefined; + for (const action of this.preferencesTabActions) { + if (action.id === id) { + action.checked = true; + selectedAction = action; + } else { + action.checked = false; + } + } + + if (selectedAction) { + this.searchWidget.inputBox.setPlaceHolder(localize('FullTextSearchPlaceholder', "Search {0}", selectedAction.descriptor.title)); + this.searchWidget.inputBox.setAriaLabel(localize('FullTextSearchPlaceholder', "Search {0}", selectedAction.descriptor.title)); + } + + this.renderBody(selectedAction?.descriptor); + + if (this.dimension) { + this.layout(this.dimension); + } + } + + private renderBody(descriptor?: IPreferencesEditorPaneDescriptor): void { + this.preferencesEditorPane.value = undefined; + DOM.clearNode(this.bodyElement); + + if (descriptor) { + const editorPane = this.instantiationService.createInstance(descriptor.ctorDescriptor.ctor); + this.preferencesEditorPane.value = editorPane; + this.bodyElement.appendChild(editorPane.getDomNode()); + } + } + + override dispose(): void { + super.dispose(); + this.preferencesTabActions.forEach(action => action.dispose()); + } +} + diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesEditorRegistry.ts b/src/vs/workbench/contrib/preferences/browser/preferencesEditorRegistry.ts new file mode 100644 index 00000000000..45975d04c19 --- /dev/null +++ b/src/vs/workbench/contrib/preferences/browser/preferencesEditorRegistry.ts @@ -0,0 +1,106 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js'; +import * as DOM from '../../../../base/browser/dom.js'; +import { Event, Emitter } from '../../../../base/common/event.js'; +import { ThemeIcon } from '../../../../base/common/themables.js'; +import { URI } from '../../../../base/common/uri.js'; +import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; +import { Registry } from '../../../../platform/registry/common/platform.js'; + +export namespace Extensions { + export const PreferencesEditorPane = 'workbench.registry.preferences.editorPanes'; +} + +export interface IPreferencesEditorPane extends IDisposable { + + getDomNode(): HTMLElement; + + layout(dimension: DOM.Dimension): void; + + search(text: string): void; + +} + +export interface IPreferencesEditorPaneDescriptor { + + /** + * The id of the view container + */ + readonly id: string; + + /** + * The title of the view container + */ + readonly title: string; + + /** + * Icon representation of the View container + */ + readonly icon?: ThemeIcon | URI; + + /** + * Order of the view container. + */ + readonly order: number; + + /** + * IViewPaneContainer Ctor to instantiate + */ + readonly ctorDescriptor: SyncDescriptor; + + /** + * Storage id to use to store the view container state. + * If not provided, it will be derived. + */ + readonly storageId?: string; +} + +export interface IPreferencesEditorPaneRegistry { + readonly onDidRegisterPreferencesEditorPanes: Event; + readonly onDidDeregisterPreferencesEditorPanes: Event; + + registerPreferencesEditorPane(descriptor: IPreferencesEditorPaneDescriptor): IDisposable; + + getPreferencesEditorPanes(): readonly IPreferencesEditorPaneDescriptor[]; +} + +class PreferencesEditorPaneRegistryImpl extends Disposable implements IPreferencesEditorPaneRegistry { + + private readonly descriptors = new Map(); + + private readonly _onDidRegisterPreferencesEditorPanes = this._register(new Emitter()); + readonly onDidRegisterPreferencesEditorPanes = this._onDidRegisterPreferencesEditorPanes.event; + + private readonly _onDidDeregisterPreferencesEditorPanes = this._register(new Emitter()); + readonly onDidDeregisterPreferencesEditorPanes = this._onDidDeregisterPreferencesEditorPanes.event; + + constructor() { + super(); + } + + registerPreferencesEditorPane(descriptor: IPreferencesEditorPaneDescriptor): IDisposable { + if (this.descriptors.has(descriptor.id)) { + throw new Error(`PreferencesEditorPane with id ${descriptor.id} already registered`); + } + this.descriptors.set(descriptor.id, descriptor); + this._onDidRegisterPreferencesEditorPanes.fire([descriptor]); + return { + dispose: () => { + if (this.descriptors.delete(descriptor.id)) { + this._onDidDeregisterPreferencesEditorPanes.fire([descriptor]); + } + } + }; + } + + getPreferencesEditorPanes(): readonly IPreferencesEditorPaneDescriptor[] { + return [...this.descriptors.values()].sort((a, b) => a.order - b.order); + } + +} + +Registry.add(Extensions.PreferencesEditorPane, new PreferencesEditorPaneRegistryImpl()); diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesIcons.ts b/src/vs/workbench/contrib/preferences/browser/preferencesIcons.ts index a1e96eb3715..411d47e559e 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesIcons.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesIcons.ts @@ -5,8 +5,11 @@ import { Codicon } from '../../../../base/common/codicons.js'; import { localize } from '../../../../nls.js'; +import { registerColor } from '../../../../platform/theme/common/colorRegistry.js'; import { registerIcon } from '../../../../platform/theme/common/iconRegistry.js'; +import { PANEL_BORDER } from '../../../common/theme.js'; +export const preferencesSashBorder = registerColor('preferences.sashBorder', PANEL_BORDER, localize('preferencesSashBorder', "The color of the Preferences editor splitview sash border.")); export const settingsScopeDropDownIcon = registerIcon('settings-folder-dropdown', Codicon.triangleDown, localize('settingsScopeDropDownIcon', 'Icon for the folder dropdown button in the split JSON Settings editor.')); export const settingsMoreActionIcon = registerIcon('settings-more-action', Codicon.gear, localize('settingsMoreActionIcon', 'Icon for the \'more actions\' action in the Settings UI.')); diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index ecbe2909568..122f426d4d5 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -666,7 +666,7 @@ export class SettingsEditor2 extends EditorPane { this._register(this.searchWidget.onInputDidChange(() => { const searchVal = this.searchWidget.getValue(); clearInputAction.enabled = !!searchVal; - this.searchInputDelayer.trigger(() => this.onSearchInputChanged()); + this.searchInputDelayer.trigger(() => this.onSearchInputChanged(true)); })); const headerControlsContainer = DOM.append(this.headerContainer, $('.settings-header-controls')); @@ -782,7 +782,7 @@ export class SettingsEditor2 extends EditorPane { if (!recursed && (!targetElement || revealFailed)) { // We'll call this event handler again after clearing the search query, // so that more settings show up in the list. - const p = this.triggerSearch(''); + const p = this.triggerSearch('', true); p.then(() => { this.searchWidget.setValue(''); this.onDidClickSetting(evt, true); @@ -1443,7 +1443,7 @@ export class SettingsEditor2 extends EditorPane { if (schemaChange && this.searchResultModel) { // If an extension's settings were just loaded and a search is active, retrigger the search so it shows up - return await this.onSearchInputChanged(); + return await this.onSearchInputChanged(false); } this.refreshTOCTree(); @@ -1455,7 +1455,7 @@ export class SettingsEditor2 extends EditorPane { // Don't restore the cached state if we already have a query value from calling _setOptions(). const cachedState = !this.viewState.query ? this.restoreCachedState() : undefined; if (cachedState?.searchQuery || this.searchWidget.getValue()) { - await this.onSearchInputChanged(); + await this.onSearchInputChanged(true); } else { this.refreshTOCTree(); this.refreshTree(); @@ -1581,7 +1581,7 @@ export class SettingsEditor2 extends EditorPane { } } - private async onSearchInputChanged(): Promise { + private async onSearchInputChanged(expandResults: boolean): Promise { if (!this.currentSettingsModel) { // Initializing search widget value return; @@ -1589,7 +1589,7 @@ export class SettingsEditor2 extends EditorPane { const query = this.searchWidget.getValue().trim(); this.viewState.query = query; - await this.triggerSearch(query.replace(/\u203A/g, ' ')); + await this.triggerSearch(query.replace(/\u203A/g, ' '), expandResults); } private parseSettingFromJSON(query: string): string | null { @@ -1614,7 +1614,7 @@ export class SettingsEditor2 extends EditorPane { } } - private async triggerSearch(query: string): Promise { + private async triggerSearch(query: string, expandResults: boolean): Promise { this.clearSearchSuggestions(); const progressRunner = this.editorProgressService.show(true, 800); this.viewState.tagFilters = new Set(); @@ -1636,7 +1636,7 @@ export class SettingsEditor2 extends EditorPane { if (query && query !== '@') { query = this.parseSettingFromJSON(query) || query; - await this.triggerFilterPreferences(query); + await this.triggerFilterPreferences(query, expandResults); this.toggleTocBySearchBehaviorType(); } else { if (this.viewState.tagFilters.size || this.viewState.extensionFilters.size || this.viewState.featureFilters.size || this.viewState.idFilters.size || this.viewState.languageFilter) { @@ -1651,14 +1651,18 @@ export class SettingsEditor2 extends EditorPane { this.searchInProgress = null; } - this.tocTree.setFocus([]); - this.viewState.filterToCategory = undefined; + if (expandResults) { + this.tocTree.setFocus([]); + this.viewState.filterToCategory = undefined; + } this.tocTreeModel.currentSearchModel = this.searchResultModel; if (this.searchResultModel) { // Added a filter model - this.tocTree.setSelection([]); - this.tocTree.expandAll(); + if (expandResults) { + this.tocTree.setSelection([]); + this.tocTree.expandAll(); + } this.refreshTOCTree(); this.renderResultCountMessages(); this.refreshTree(); @@ -1697,7 +1701,7 @@ export class SettingsEditor2 extends EditorPane { return filterModel; } - private async triggerFilterPreferences(query: string): Promise { + private async triggerFilterPreferences(query: string, expandResults: boolean): Promise { if (this.searchInProgress) { this.searchInProgress.dispose(true); this.searchInProgress = null; @@ -1721,7 +1725,7 @@ export class SettingsEditor2 extends EditorPane { // Update UI only after all the search results are in // ref https://github.com/microsoft/vscode/issues/224946 - this.onDidFinishSearch(); + this.onDidFinishSearch(expandResults); if (remoteResults?.filterMatches.length) { if (this.aiSettingsSearchService.isEnabled() && !searchInProgress.token.isCancellationRequested) { @@ -1746,13 +1750,14 @@ export class SettingsEditor2 extends EditorPane { }); } - private onDidFinishSearch() { + private onDidFinishSearch(expandResults: boolean) { this.tocTreeModel.currentSearchModel = this.searchResultModel; - this.tocTreeModel.update(); - this.tocTree.setFocus([]); - this.viewState.filterToCategory = undefined; - this.tocTree.expandAll(); - this.settingsTree.scrollTop = 0; + if (expandResults) { + this.tocTree.setFocus([]); + this.viewState.filterToCategory = undefined; + this.tocTree.expandAll(); + this.settingsTree.scrollTop = 0; + } this.refreshTOCTree(); this.renderTree(undefined, true); } diff --git a/src/vs/workbench/contrib/preferences/common/preferences.ts b/src/vs/workbench/contrib/preferences/common/preferences.ts index bb49a5e8b18..6f6e08b06c3 100644 --- a/src/vs/workbench/contrib/preferences/common/preferences.ts +++ b/src/vs/workbench/contrib/preferences/common/preferences.ts @@ -50,6 +50,9 @@ export interface IRemoteSearchProvider extends ISearchProvider { setFilter(filter: string): void; } +export const PREFERENCES_EDITOR_COMMAND_OPEN = 'workbench.preferences.action.openPreferencesEditor'; +export const CONTEXT_PREFERENCES_SEARCH_FOCUS = new RawContextKey('inPreferencesSearch', false); + export const SETTINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS = 'settings.action.clearSearchResults'; export const SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU = 'settings.action.showContextMenu'; export const SETTINGS_EDITOR_COMMAND_SUGGEST_FILTERS = 'settings.action.suggestFilters'; @@ -215,6 +218,7 @@ export function wordifyKey(key: string): string { key = key .replace(/\.([a-z0-9])/g, (_, p1) => ` \u203A ${p1.toUpperCase()}`) // Replace dot with spaced '>' .replace(/([a-z0-9])([A-Z])/g, '$1 $2') // Camel case to spacing, fooBar => foo Bar + .replace(/([A-Z]{1,})([A-Z][a-z])/g, '$1 $2') // Split consecutive capitals letters, AISearch => AI Search .replace(/^[a-z]/g, match => match.toUpperCase()) // Upper casing all first letters, foo => Foo .replace(/\b\w+\b/g, match => { // Upper casing known acronyms return knownAcronyms.has(match.toLowerCase()) ? diff --git a/src/vs/workbench/contrib/processExplorer/browser/media/processExplorer.css b/src/vs/workbench/contrib/processExplorer/browser/media/processExplorer.css index 9737faebbdf..1990ca43167 100644 --- a/src/vs/workbench/contrib/processExplorer/browser/media/processExplorer.css +++ b/src/vs/workbench/contrib/processExplorer/browser/media/processExplorer.css @@ -29,6 +29,13 @@ text-overflow: ellipsis; } +.process-explorer .monaco-tl-twistie.force-no-twistie { + background-image: none !important; + width: 0 !important; + padding-right: 0 !important; + visibility: hidden; +} + .process-explorer .row .cell.name { text-align: left; flex-grow: 1; @@ -53,6 +60,7 @@ border-bottom-right-radius: 5px; border-bottom-left-radius: 5px; } + .mac:not(.fullscreen).macos-bigsur-or-newer .process-explorer .monaco-list:focus::before { /* macOS Big Sur increased rounded corners size */ border-bottom-right-radius: 10px; diff --git a/src/vs/workbench/contrib/processExplorer/browser/processExplorerControl.ts b/src/vs/workbench/contrib/processExplorer/browser/processExplorerControl.ts index 2df28599538..b80119c7891 100644 --- a/src/vs/workbench/contrib/processExplorer/browser/processExplorerControl.ts +++ b/src/vs/workbench/contrib/processExplorer/browser/processExplorerControl.ts @@ -170,6 +170,8 @@ class ProcessHeaderTreeRenderer implements ITreeRenderer .margin { + height: 22px; + border-left: 3px; + border-left-style: solid; +} + +.scm-view .monaco-list-row .monaco-tl-twistie.collapsed + .monaco-tl-contents > .history-item > .margin { + visibility: hidden; +} + +.scm-view .monaco-list-row .monaco-tl-twistie:not(.collapsed) + .monaco-tl-contents > .history-item > .margin { + visibility: visible; +} + .scm-view .monaco-list-row .history-item > .graph-container { display: flex; flex-shrink: 0; @@ -158,6 +172,7 @@ .scm-view .monaco-list-row .history-item > .label-container { display: flex; flex-shrink: 0; + margin-left: 4px; gap: 4px; } @@ -212,6 +227,10 @@ font-weight: 500; } +.scm-view .monaco-list-row .history-item > .actions { + margin-left: 4px; +} + .scm-view .monaco-list-row .history > .name, .scm-view .monaco-list-row .resource-group > .name { flex: 1; @@ -233,7 +252,8 @@ overflow: hidden; } -.scm-view .monaco-list-row .resource > .name > .monaco-icon-label::after { +.scm-view .monaco-list-row .resource > .name > .monaco-icon-label::after, +.scm-view .monaco-list-row .history-item-change > .label-container > .monaco-icon-label::after { margin-right: 3px; } @@ -256,7 +276,8 @@ .scm-view .monaco-list .monaco-list-row .resource-group > .actions, .scm-view .monaco-list .monaco-list-row .resource > .name > .monaco-icon-label > .actions, -.scm-view .monaco-list .monaco-list-row .history-item > .actions { +.scm-view .monaco-list .monaco-list-row .history-item > .actions, +.scm-view .monaco-list .monaco-list-row .history-item-change > .label-container > .monaco-icon-label > .actions { display: none; max-width: fit-content; } @@ -267,7 +288,9 @@ .scm-view .monaco-list .monaco-list-row.focused .resource > .name > .monaco-icon-label > .actions, .scm-view .monaco-list:not(.selection-multiple) .monaco-list-row .resource:hover > .actions, .scm-view .monaco-list .monaco-list-row:hover .history-item > .actions, -.scm-view .monaco-list .monaco-list-row.focused .history-item > .actions { +.scm-view .monaco-list .monaco-list-row.focused .history-item > .actions, +.scm-view .monaco-list .monaco-list-row:hover .history-item-change > .label-container > .monaco-icon-label > .actions, +.scm-view .monaco-list .monaco-list-row.focused .history-item-change > .label-container > .monaco-icon-label > .actions { display: block; } @@ -593,6 +616,10 @@ text-overflow: ellipsis; } +.scm-history-view .monaco-list-row > .monaco-tl-row > .monaco-tl-twistie.force-no-twistie { + display: none !important; +} + .scm-history-view .scm-provider .label-name { font-weight: bold; } @@ -615,9 +642,34 @@ flex-grow: 1; } +.scm-view .monaco-list-row .history-item-change { + display: flex; + align-items: center; +} + +.scm-view .monaco-list-row .history-item-change > .margin { + height: 22px; + border-left-width: 3px; + border-left-style: solid; +} +.scm-view .monaco-list-row .history-item-change > .graph-placeholder { + height: 22px; +} + +.scm-view .monaco-list-row .history-item-change > .label-container { + display: flex; + flex: 1; + overflow: hidden; +} + +.scm-view .monaco-list-row .history-item-change > .label-container > .monaco-icon-label { + flex-grow: 1; +} + .scm-history-view .history-item-load-more { display: flex; height: 22px; + padding-left: 3px; } .scm-history-view .history-item-load-more .graph-placeholder { diff --git a/src/vs/workbench/contrib/scm/browser/quickDiffWidget.ts b/src/vs/workbench/contrib/scm/browser/quickDiffWidget.ts index 1d16201b68f..d27c7736c8b 100644 --- a/src/vs/workbench/contrib/scm/browser/quickDiffWidget.ts +++ b/src/vs/workbench/contrib/scm/browser/quickDiffWidget.ts @@ -229,7 +229,9 @@ class QuickDiffWidget extends PeekViewWidget { const lineHeight = this.editor.getOption(EditorOption.lineHeight); const editorHeight = this.editor.getLayoutInfo().height; const editorHeightInLines = Math.floor(editorHeight / lineHeight); - const height = Math.min(getChangeHeight(change) + /* padding */ 8, Math.floor(editorHeightInLines / 3)); + const height = Math.min( + getChangeHeight(change) + 2 /* arrow, frame, header */ + 6 /* 3 lines above/below the change */, + Math.floor(editorHeightInLines / 3)); this.renderTitle(); this.updateDropdown(); @@ -250,7 +252,10 @@ class QuickDiffWidget extends PeekViewWidget { } this._actionbarWidget!.context = [diffEditorModel.modified.uri, providerSpecificChanges, contextIndex]; if (usePosition) { - this.show(position, height); + // In order to account for the 1px border-top of the content element we + // have to add 1px. The pixel value needs to be expressed as a fraction + // of the line height. + this.show(position, height + (1 / lineHeight)); this.editor.setPosition(position); this.editor.focus(); } @@ -392,7 +397,7 @@ class QuickDiffWidget extends PeekViewWidget { verticalHasArrows: false, horizontalHasArrows: false }, - scrollBeyondLastLine: true, + scrollBeyondLastLine: false, stickyScroll: { enabled: false } }; diff --git a/src/vs/workbench/contrib/scm/browser/scmHistory.ts b/src/vs/workbench/contrib/scm/browser/scmHistory.ts index fbe1f7a4c33..8f74a15cae7 100644 --- a/src/vs/workbench/contrib/scm/browser/scmHistory.ts +++ b/src/vs/workbench/contrib/scm/browser/scmHistory.ts @@ -353,3 +353,19 @@ export function toISCMHistoryItemViewModelArray( return viewModels; } + +export function getHistoryItemColor(historyItemViewModel: ISCMHistoryItemViewModel): string { + const historyItem = historyItemViewModel.historyItem; + const inputSwimlanes = historyItemViewModel.inputSwimlanes; + const outputSwimlanes = historyItemViewModel.outputSwimlanes; + + // Find the history item in the input swimlanes + const inputIndex = inputSwimlanes.findIndex(node => node.id === historyItem.id); + + // Circle index - use the input swimlane index if present, otherwise add it to the end + const circleIndex = inputIndex !== -1 ? inputIndex : inputSwimlanes.length; + + // Circle color - use the output swimlane color if present, otherwise the input swimlane color + return circleIndex < outputSwimlanes.length ? outputSwimlanes[circleIndex].color : + circleIndex < inputSwimlanes.length ? inputSwimlanes[circleIndex].color : historyItemRefColor; +} diff --git a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts index 8953f4ac069..3e0f25163ea 100644 --- a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts @@ -10,12 +10,12 @@ import { IHoverAction, IHoverOptions, IManagedHoverTooltipMarkdownString } from import { IHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegate.js'; import { IconLabel } from '../../../../base/browser/ui/iconLabel/iconLabel.js'; import { IIdentityProvider, IKeyboardNavigationLabelProvider, IListVirtualDelegate } from '../../../../base/browser/ui/list/list.js'; -import { LabelFuzzyScore } from '../../../../base/browser/ui/tree/abstractTree.js'; +import { LabelFuzzyScore, RenderIndentGuides } from '../../../../base/browser/ui/tree/abstractTree.js'; import { IAsyncDataSource, ITreeContextMenuEvent, ITreeNode, ITreeRenderer } from '../../../../base/browser/ui/tree/tree.js'; import { fromNow, safeIntl } from '../../../../base/common/date.js'; import { createMatches, FuzzyScore, IMatch } from '../../../../base/common/filters.js'; import { MarkdownString } from '../../../../base/common/htmlContent.js'; -import { Disposable, DisposableStore, IDisposable, MutableDisposable } from '../../../../base/common/lifecycle.js'; +import { combinedDisposable, Disposable, DisposableStore, IDisposable, MutableDisposable } from '../../../../base/common/lifecycle.js'; import { autorun, autorunWithStore, derived, IObservable, observableValue, waitForState, constObservable, latestChangedValue, observableFromEvent, runOnChange, observableSignal } from '../../../../base/common/observable.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { localize } from '../../../../nls.js'; @@ -31,9 +31,9 @@ import { asCssVariable, ColorIdentifier, foreground } from '../../../../platform import { IThemeService } from '../../../../platform/theme/common/themeService.js'; import { IViewPaneOptions, ViewAction, ViewPane, ViewPaneShowActions } from '../../../browser/parts/views/viewPane.js'; import { IViewDescriptorService, ViewContainerLocation } from '../../../common/views.js'; -import { renderSCMHistoryItemGraph, toISCMHistoryItemViewModelArray, SWIMLANE_WIDTH, renderSCMHistoryGraphPlaceholder, historyItemHoverDeletionsForeground, historyItemHoverLabelForeground, historyItemHoverAdditionsForeground, historyItemHoverDefaultLabelForeground, historyItemHoverDefaultLabelBackground } from './scmHistory.js'; -import { getHistoryItemEditorTitle, getProviderKey, isSCMHistoryItemLoadMoreTreeElement, isSCMHistoryItemViewModelTreeElement, isSCMRepository } from './util.js'; -import { ISCMHistoryItem, ISCMHistoryItemRef, ISCMHistoryItemViewModel, ISCMHistoryProvider, SCMHistoryItemLoadMoreTreeElement, SCMHistoryItemViewModelTreeElement } from '../common/history.js'; +import { renderSCMHistoryItemGraph, toISCMHistoryItemViewModelArray, SWIMLANE_WIDTH, renderSCMHistoryGraphPlaceholder, historyItemHoverDeletionsForeground, historyItemHoverLabelForeground, historyItemHoverAdditionsForeground, historyItemHoverDefaultLabelForeground, historyItemHoverDefaultLabelBackground, getHistoryItemColor } from './scmHistory.js'; +import { getHistoryItemEditorTitle, getProviderKey, isSCMHistoryItemChangeViewModelTreeElement, isSCMHistoryItemLoadMoreTreeElement, isSCMHistoryItemViewModelTreeElement, isSCMRepository } from './util.js'; +import { ISCMHistoryItem, ISCMHistoryItemChange, ISCMHistoryItemRef, ISCMHistoryItemViewModel, ISCMHistoryProvider, SCMHistoryItemChangeViewModelTreeElement, SCMHistoryItemLoadMoreTreeElement, SCMHistoryItemViewModelTreeElement } from '../common/history.js'; import { HISTORY_VIEW_PANE_ID, ISCMProvider, ISCMRepository, ISCMService, ISCMViewService } from '../common/scm.js'; import { IListAccessibilityProvider } from '../../../../base/browser/ui/list/listWidget.js'; import { stripIcons } from '../../../../base/common/iconLabels.js'; @@ -62,13 +62,19 @@ import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hover import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; import { groupBy as groupBy2 } from '../../../../base/common/collections.js'; -import { getFlatContextMenuActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; +import { getActionBarActions, getFlatContextMenuActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; +import { IResourceLabel, ResourceLabels } from '../../../browser/labels.js'; +import { FileKind } from '../../../../platform/files/common/files.js'; +import { WorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; +import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; +import { basename } from '../../../../base/common/path.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; +import { ScmHistoryItemResolver } from '../../multiDiffEditor/browser/scmMultiDiffSourceResolver.js'; const PICK_REPOSITORY_ACTION_ID = 'workbench.scm.action.graph.pickRepository'; const PICK_HISTORY_ITEM_REFS_ACTION_ID = 'workbench.scm.action.graph.pickHistoryItemRefs'; -type TreeElement = SCMHistoryItemViewModelTreeElement | SCMHistoryItemLoadMoreTreeElement; +type TreeElement = SCMHistoryItemViewModelTreeElement | SCMHistoryItemLoadMoreTreeElement | SCMHistoryItemChangeViewModelTreeElement; class SCMRepositoryActionViewItem extends ActionViewItem { constructor(private readonly _repository: ISCMRepository, action: IAction, options?: IDropdownMenuActionViewItemOptions) { @@ -239,8 +245,15 @@ registerAction2(class extends Action2 { super({ id: 'workbench.scm.action.graph.viewChanges', title: localize('openChanges', "Open Changes"), + icon: Codicon.diffMultiple, f1: false, menu: [ + { + id: MenuId.SCMHistoryItemContext, + when: ContextKeyExpr.equals('config.multiDiffEditor.experimental.enabled', true), + group: 'inline', + order: 1 + }, { id: MenuId.SCMHistoryItemContext, when: ContextKeyExpr.equals('config.multiDiffEditor.experimental.enabled', true), @@ -269,25 +282,47 @@ registerAction2(class extends Action2 { } } - const historyItemParentId = historyItemLast.parentIds.length > 0 ? historyItemLast.parentIds[0] : undefined; - const historyItemChanges = await historyProvider?.provideHistoryItemChanges(historyItem.id, historyItemParentId); - - if (!historyItemChanges?.length) { - return; - } - const title = historyItems.length === 1 ? getHistoryItemEditorTitle(historyItem) : localize('historyItemChangesEditorTitle', "All Changes ({0} ↔ {1})", historyItemLast.displayId ?? historyItemLast.id, historyItem.displayId ?? historyItem.id); - const rootUri = provider.rootUri; - const path = rootUri ? rootUri.path : provider.label; - const multiDiffSourceUri = URI.from({ scheme: 'scm-history-item', path: `${path}/${historyItemParentId}..${historyItem.id}` }, true); - - commandService.executeCommand('_workbench.openMultiDiffEditor', { title, multiDiffSourceUri, resources: historyItemChanges }); + const multiDiffSourceUri = ScmHistoryItemResolver.getMultiDiffSourceUri(provider.id, historyItem); + commandService.executeCommand('_workbench.openMultiDiffEditor', { title, multiDiffSourceUri }); } }); +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.scm.action.graph.openFile', + title: localize('openFile', "Open File"), + icon: Codicon.goToFile, + f1: false, + menu: [ + { + id: MenuId.SCMHistoryItemChangeContext, + group: 'inline', + order: 1 + }, + ] + }); + } + + override async run(accessor: ServicesAccessor, historyItem: ISCMHistoryItem, historyItemChange: ISCMHistoryItemChange) { + const editorService = accessor.get(IEditorService); + + if (!historyItem || !historyItemChange.modifiedUri) { + return; + } + + await editorService.openEditor({ + resource: historyItemChange.modifiedUri, + label: `${basename(historyItemChange.modifiedUri.fsPath)} (${historyItem.displayId ?? historyItem.id})`, + }); + } +}); + + class ListDelegate implements IListVirtualDelegate { getHeight(): number { @@ -297,6 +332,8 @@ class ListDelegate implements IListVirtualDelegate { getTemplateId(element: TreeElement): string { if (isSCMHistoryItemViewModelTreeElement(element)) { return HistoryItemRenderer.TEMPLATE_ID; + } else if (isSCMHistoryItemChangeViewModelTreeElement(element)) { + return HistoryItemChangeRenderer.TEMPLATE_ID; } else if (isSCMHistoryItemLoadMoreTreeElement(element)) { return HistoryItemLoadMoreRenderer.TEMPLATE_ID; } else { @@ -307,8 +344,10 @@ class ListDelegate implements IListVirtualDelegate { interface HistoryItemTemplate { readonly element: HTMLElement; + readonly margin: HTMLElement; readonly label: IconLabel; readonly graphContainer: HTMLElement; + readonly actionBar: WorkbenchToolBar; readonly labelContainer: HTMLElement; readonly elementDisposables: DisposableStore; readonly disposables: IDisposable; @@ -324,10 +363,14 @@ class HistoryItemRenderer implements ITreeRenderer('scm.graph.badges', 'filter', this._configurationService); @@ -338,13 +381,18 @@ class HistoryItemRenderer implements ITreeRenderer, index: number, templateData: HistoryItemTemplate, height: number | undefined): void { @@ -357,6 +405,8 @@ class HistoryItemRenderer implements ITreeRenderer { + static readonly TEMPLATE_ID = 'history-item-change'; + get templateId(): string { return HistoryItemChangeRenderer.TEMPLATE_ID; } + + constructor( + private readonly resourceLabels: ResourceLabels, + @ICommandService private readonly _commandService: ICommandService, + @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @IContextMenuService private readonly _contextMenuService: IContextMenuService, + @IKeybindingService private readonly _keybindingService: IKeybindingService, + @IMenuService private readonly _menuService: IMenuService, + @ITelemetryService private readonly _telemetryService: ITelemetryService, + ) { } + + renderTemplate(container: HTMLElement): HistoryItemChangeTemplate { + // hack + (container.parentElement!.parentElement!.querySelector('.monaco-tl-twistie')! as HTMLElement).classList.add('force-no-twistie'); + + const element = append(container, $('.history-item-change')); + const margin = append(element, $('.margin')); + const graphPlaceholder = append(element, $('.graph-placeholder')); + + const labelContainer = append(element, $('.label-container')); + const resourceLabel = this.resourceLabels.create(labelContainer, { + supportDescriptionHighlights: true, supportHighlights: true + }); + + const disposables = new DisposableStore(); + const actionsContainer = append(resourceLabel.element, $('.actions')); + const actionBar = new WorkbenchToolBar(actionsContainer, undefined, this._menuService, this._contextKeyService, this._contextMenuService, this._keybindingService, this._commandService, this._telemetryService); + disposables.add(actionBar); + + return { element, margin, graphPlaceholder, resourceLabel, actionBar, disposables }; + } + + renderElement(element: ITreeNode, index: number, templateData: HistoryItemChangeTemplate, height: number | undefined): void { + const historyItemViewModel = element.element.historyItemViewModel; + const historyItemChange = element.element.historyItemChange; + + templateData.margin.style.borderLeftColor = asCssVariable(getHistoryItemColor(historyItemViewModel)); + + templateData.graphPlaceholder.textContent = ''; + templateData.graphPlaceholder.style.width = `${SWIMLANE_WIDTH * (element.element.graphColumns.length + 1)}px`; + templateData.graphPlaceholder.appendChild(renderSCMHistoryGraphPlaceholder(element.element.graphColumns)); + + const uri = historyItemChange.uri; + templateData.resourceLabel.setFile(uri, { fileDecorations: { colors: false, badges: true }, fileKind: FileKind.FILE, hidePath: false }); + + const actions = this._menuService.getMenuActions( + MenuId.SCMHistoryItemChangeContext, + this._contextKeyService, + { arg: historyItemViewModel.historyItem, shouldForwardArgs: true }); + templateData.actionBar.context = historyItemChange; + templateData.actionBar.setActions(getActionBarActions(actions, 'inline').primary); + } + + disposeTemplate(templateData: HistoryItemChangeTemplate): void { + templateData.disposables.dispose(); + } +} + interface LoadMoreTemplate { readonly element: HTMLElement; readonly graphPlaceholder: HTMLElement; @@ -586,7 +713,7 @@ class HistoryItemLoadMoreRenderer implements ITreeRenderer, index: number, templateData: LoadMoreTemplate, height: number | undefined): void { @@ -685,6 +812,10 @@ class SCMHistoryTreeIdentityProvider implements IIdentityProvider { const provider = element.repository.provider; const historyItem = element.historyItemViewModel.historyItem; return `historyItem:${provider.id}/${historyItem.id}/${historyItem.parentIds.join(',')}`; + } else if (isSCMHistoryItemChangeViewModelTreeElement(element)) { + const provider = element.repository.provider; + const historyItem = element.historyItemViewModel.historyItem; + return `historyItemChange:${provider.id}/${historyItem.id}/${historyItem.parentIds.join(',')}/${element.historyItemChange.uri.fsPath}`; } else if (isSCMHistoryItemLoadMoreTreeElement(element)) { const provider = element.repository.provider; return `historyItemLoadMore:${provider.id}}`; @@ -715,31 +846,45 @@ class SCMHistoryTreeKeyboardNavigationLabelProvider implements IKeyboardNavigati class SCMHistoryTreeDataSource extends Disposable implements IAsyncDataSource { async getChildren(inputOrElement: SCMHistoryViewModel | TreeElement): Promise> { - if (!(inputOrElement instanceof SCMHistoryViewModel)) { - return []; - } - - // History items const children: TreeElement[] = []; - const historyItems = await inputOrElement.getHistoryItems(); - children.push(...historyItems); - // Load More element - const repository = inputOrElement.repository.get(); - const lastHistoryItem = historyItems.at(-1); - if (repository && lastHistoryItem && lastHistoryItem.historyItemViewModel.outputSwimlanes.length > 0) { - children.push({ - repository, - graphColumns: lastHistoryItem.historyItemViewModel.outputSwimlanes, - type: 'historyItemLoadMore' - } satisfies SCMHistoryItemLoadMoreTreeElement); + if (inputOrElement instanceof SCMHistoryViewModel) { + // History items + const historyItems = await inputOrElement.getHistoryItems(); + children.push(...historyItems); + + // Load More element + const repository = inputOrElement.repository.get(); + const lastHistoryItem = historyItems.at(-1); + if (repository && lastHistoryItem && lastHistoryItem.historyItemViewModel.outputSwimlanes.length > 0) { + children.push({ + repository, + graphColumns: lastHistoryItem.historyItemViewModel.outputSwimlanes, + type: 'historyItemLoadMore' + } satisfies SCMHistoryItemLoadMoreTreeElement); + } + } else if (inputOrElement.type === 'historyItemViewModel') { + // History item changes + const historyItem = inputOrElement.historyItemViewModel.historyItem; + const historyItemParentId = historyItem.parentIds.length > 0 ? historyItem.parentIds[0] : undefined; + + const historyProvider = inputOrElement.repository.provider.historyProvider.get(); + const historyItemChanges = await historyProvider?.provideHistoryItemChanges(historyItem.id, historyItemParentId) ?? []; + + children.push(...historyItemChanges.map(change => ({ + repository: inputOrElement.repository, + historyItemViewModel: inputOrElement.historyItemViewModel, + historyItemChange: change, + graphColumns: inputOrElement.historyItemViewModel.outputSwimlanes, + type: 'historyItemChangeViewModel' + } satisfies SCMHistoryItemChangeViewModelTreeElement))); } return children; } hasChildren(inputOrElement: SCMHistoryViewModel | TreeElement): boolean { - return inputOrElement instanceof SCMHistoryViewModel; + return inputOrElement instanceof SCMHistoryViewModel || inputOrElement.type === 'historyItemViewModel'; } } @@ -1293,7 +1438,7 @@ export class SCMHistoryViewPane extends ViewPane { protected override renderBody(container: HTMLElement): void { super.renderBody(container); - this._treeContainer = append(container, $('.scm-view.scm-history-view')); + this._treeContainer = append(container, $('.scm-view.scm-history-view.show-file-icons')); this._treeContainer.classList.add('file-icon-themable-tree'); this._createTree(this._treeContainer); @@ -1518,6 +1663,9 @@ export class SCMHistoryViewPane extends ViewPane { const historyItemHoverDelegate = this.instantiationService.createInstance(HistoryItemHoverDelegate, this.viewDescriptorService.getViewLocationById(this.id)); this._register(historyItemHoverDelegate); + const resourceLabels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this.onDidChangeBodyVisibility }); + this._register(resourceLabels); + this._treeDataSource = this.instantiationService.createInstance(SCMHistoryTreeDataSource); this._register(this._treeDataSource); @@ -1528,19 +1676,18 @@ export class SCMHistoryViewPane extends ViewPane { new ListDelegate(), [ this.instantiationService.createInstance(HistoryItemRenderer, historyItemHoverDelegate), - this.instantiationService.createInstance( - HistoryItemLoadMoreRenderer, - this._repositoryIsLoadingMore, - () => this._loadMore()), + this.instantiationService.createInstance(HistoryItemChangeRenderer, resourceLabels), + this.instantiationService.createInstance(HistoryItemLoadMoreRenderer, this._repositoryIsLoadingMore, () => this._loadMore()), ], this._treeDataSource, { accessibilityProvider: new SCMHistoryTreeAccessibilityProvider(), identityProvider: this._treeIdentityProvider, - collapseByDefault: (e: unknown) => false, + collapseByDefault: (e: unknown) => true, keyboardNavigationLabelProvider: new SCMHistoryTreeKeyboardNavigationLabelProvider(), horizontalScrolling: false, multipleSelectionSupport: false, + renderIndentGuides: RenderIndentGuides.None } ) as WorkbenchAsyncDataTree; this._register(this._tree); @@ -1565,25 +1712,39 @@ export class SCMHistoryViewPane extends ViewPane { private async _onDidOpen(e: IOpenEvent): Promise { if (!e.element) { return; - } else if (isSCMHistoryItemViewModelTreeElement(e.element)) { + } else if (isSCMHistoryItemChangeViewModelTreeElement(e.element)) { + const historyItemChange = e.element.historyItemChange; const historyItem = e.element.historyItemViewModel.historyItem; + const historyItemDisplayId = historyItem.displayId ?? historyItem.id; + const historyItemParentId = historyItem.parentIds.length > 0 ? historyItem.parentIds[0] : undefined; + const historyItemParentDisplayId = historyItemParentId && historyItem.displayId + ? historyItemParentId.substring(0, historyItem.displayId.length) + : historyItemParentId; - const historyProvider = e.element.repository.provider.historyProvider.get(); - const historyItemChanges = await historyProvider?.provideHistoryItemChanges(historyItem.id, historyItemParentId); - if (historyItemChanges) { - const title = getHistoryItemEditorTitle(historyItem); - const rootUri = e.element.repository.provider.rootUri; - const path = rootUri ? rootUri.path : e.element.repository.provider.label; - const multiDiffSourceUri = URI.from({ scheme: 'scm-history-item', path: `${path}/${historyItemParentId}..${historyItem.id}` }, true); + if (historyItemChange.originalUri && historyItemChange.modifiedUri) { + // Diff Editor + const originalUriTitle = `${basename(historyItemChange.originalUri.fsPath)} (${historyItemParentDisplayId})`; + const modifiedUriTitle = `${basename(historyItemChange.modifiedUri.fsPath)} (${historyItemDisplayId})`; + const title = `${originalUriTitle} ↔ ${modifiedUriTitle}`; await this._editorService.openEditor({ label: title, - multiDiffSource: multiDiffSourceUri, - resources: historyItemChanges.map(c => ({ - original: { resource: c.originalUri }, - modified: { resource: c.modifiedUri } - })), + original: { resource: historyItemChange.originalUri }, + modified: { resource: historyItemChange.modifiedUri }, + options: e.editorOptions + }); + } else if (historyItemChange.modifiedUri) { + await this._editorService.openEditor({ + label: `${basename(historyItemChange.modifiedUri.fsPath)} (${historyItemDisplayId})`, + resource: historyItemChange.modifiedUri, + options: e.editorOptions + }); + } else if (historyItemChange.originalUri) { + // Editor (Deleted) + await this._editorService.openEditor({ + label: `${basename(historyItemChange.originalUri.fsPath)} (${historyItemParentDisplayId})`, + resource: historyItemChange.originalUri, options: e.editorOptions }); } diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 83f8a15401d..bad104c327f 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -21,7 +21,7 @@ import { IContextKeyService, IContextKey, ContextKeyExpr, RawContextKey } from ' import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; import { MenuItemAction, IMenuService, registerAction2, MenuId, IAction2Options, MenuRegistry, Action2, IMenu } from '../../../../platform/actions/common/actions.js'; -import { IAction, ActionRunner, Action, Separator, IActionRunner, toAction } from '../../../../base/common/actions.js'; +import { IAction, ActionRunner, Action, Separator, IActionRunner, toAction, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification } from '../../../../base/common/actions.js'; import { ActionBar, IActionViewItemProvider } from '../../../../base/browser/ui/actionbar/actionbar.js'; import { IThemeService, IFileIconTheme } from '../../../../platform/theme/common/themeService.js'; import { isSCMResource, isSCMResourceGroup, isSCMRepository, isSCMInput, collectContextMenuActions, getActionViewItemProvider, isSCMActionButton, isSCMViewService, isSCMResourceNode, connectPrimaryMenu } from './util.js'; @@ -108,6 +108,9 @@ import { observableConfigValue } from '../../../../platform/observable/common/pl import { AccessibilityVerbositySettingId } from '../../accessibility/browser/accessibilityConfiguration.js'; import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js'; import { AccessibilityCommandId } from '../../accessibility/common/accessibilityCommands.js'; +import { ChatContextKeys } from '../../chat/common/chatContextKeys.js'; +import product from '../../../../platform/product/common/product.js'; +import { CHAT_SETUP_ACTION_ID } from '../../chat/browser/actions/chatActions.js'; type TreeElement = ISCMRepository | ISCMInput | ISCMActionButton | ISCMResourceGroup | ISCMResource | IResourceNode; @@ -1324,13 +1327,53 @@ registerAction2(CollapseAllRepositoriesAction); registerAction2(ExpandAllRepositoriesAction); const enum SCMInputWidgetCommandId { - CancelAction = 'scm.input.cancelAction' + CancelAction = 'scm.input.cancelAction', + SetupAction = 'scm.input.triggerSetup' } const enum SCMInputWidgetStorageKey { LastActionId = 'scm.input.lastActionId' } +registerAction2(class extends Action2 { + constructor() { + super({ + id: SCMInputWidgetCommandId.SetupAction, + title: localize('scmInputGenerateCommitMessage', "Generate Commit Message with Copilot"), + icon: Codicon.sparkle, + f1: false, + menu: { + id: MenuId.SCMInputBox, + when: ContextKeyExpr.and( + ChatContextKeys.Setup.hidden.negate(), + ChatContextKeys.Setup.disabled.negate(), + ChatContextKeys.Setup.installed.negate(), + ContextKeyExpr.equals('scmProvider', 'git') + ) + } + }); + } + + override async run(accessor: ServicesAccessor, ...args: any[]): Promise { + const commandService = accessor.get(ICommandService); + const telemetryService = accessor.get(ITelemetryService); + + telemetryService.publicLog2('workbenchActionExecuted', { id: CHAT_SETUP_ACTION_ID, from: 'scmInput' }); + + const result = await commandService.executeCommand(CHAT_SETUP_ACTION_ID); + if (!result) { + return; + } + + const command = product.defaultChatAgent?.generateCommitMessageCommand; + if (!command) { + return; + } + + await commandService.executeCommand(command, ...args); + } +}); + class SCMInputWidgetActionRunner extends ActionRunner { private readonly _runningActions = new Set(); @@ -1374,7 +1417,10 @@ class SCMInputWidgetActionRunner extends ActionRunner { // Save last action if (this._runningActions.size === 0) { - this.storageService.store(SCMInputWidgetStorageKey.LastActionId, action.id, StorageScope.PROFILE, StorageTarget.USER); + const actionId = action.id === SCMInputWidgetCommandId.SetupAction + ? product.defaultChatAgent?.generateCommitMessageCommand ?? action.id + : action.id; + this.storageService.store(SCMInputWidgetStorageKey.LastActionId, actionId, StorageScope.PROFILE, StorageTarget.USER); } } } @@ -1446,7 +1492,9 @@ class SCMInputWidgetToolbar extends WorkbenchToolBar { let primaryAction: IAction | undefined = undefined; - if (actions.length === 1) { + if ((this.actionRunner as SCMInputWidgetActionRunner).runningActions.size !== 0) { + primaryAction = this._cancelAction; + } else if (actions.length === 1) { primaryAction = actions[0]; } else if (actions.length > 1) { const lastActionId = this.storageService.get(SCMInputWidgetStorageKey.LastActionId, StorageScope.PROFILE, ''); @@ -2572,13 +2620,17 @@ export class SCMViewPane extends ViewPane { const element = e.element; let context: any = element; let actions: IAction[] = []; + + const disposables = new DisposableStore(); let actionRunner: IActionRunner = new RepositoryPaneActionRunner(() => this.getSelectedResources()); + disposables.add(actionRunner); if (isSCMRepository(element)) { const menus = this.scmViewService.menus.getRepositoryMenus(element.provider); const menu = menus.repositoryContextMenu; context = element.provider; actionRunner = new RepositoryActionRunner(() => this.getSelectedRepositories()); + disposables.add(actionRunner); actions = collectContextMenuActions(menu); } else if (isSCMInput(element) || isSCMActionButton(element)) { // noop @@ -2602,14 +2654,14 @@ export class SCMViewPane extends ViewPane { } } - actionRunner.onWillRun(() => this.tree.domFocus()); + disposables.add(actionRunner.onWillRun(() => this.tree.domFocus())); this.contextMenuService.showContextMenu({ actionRunner, getAnchor: () => e.anchor, getActions: () => actions, getActionsContext: () => context, - onHide: () => actionRunner.dispose() + onHide: () => disposables.dispose() }); } diff --git a/src/vs/workbench/contrib/scm/browser/util.ts b/src/vs/workbench/contrib/scm/browser/util.ts index 73a452a4efd..68eddea4ab2 100644 --- a/src/vs/workbench/contrib/scm/browser/util.ts +++ b/src/vs/workbench/contrib/scm/browser/util.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ISCMHistoryItem, ISCMHistoryItemRef, SCMHistoryItemLoadMoreTreeElement, SCMHistoryItemViewModelTreeElement } from '../common/history.js'; +import { ISCMHistoryItem, ISCMHistoryItemRef, SCMHistoryItemChangeViewModelTreeElement, SCMHistoryItemLoadMoreTreeElement, SCMHistoryItemViewModelTreeElement } from '../common/history.js'; import { ISCMResource, ISCMRepository, ISCMResourceGroup, ISCMInput, ISCMActionButton, ISCMViewService, ISCMProvider } from '../common/scm.js'; import { IMenu, MenuItemAction } from '../../../../platform/actions/common/actions.js'; import { IActionViewItemProvider } from '../../../../base/browser/ui/actionbar/actionbar.js'; @@ -55,6 +55,10 @@ export function isSCMHistoryItemLoadMoreTreeElement(element: any): element is SC return (element as SCMHistoryItemLoadMoreTreeElement).type === 'historyItemLoadMore'; } +export function isSCMHistoryItemChangeViewModelTreeElement(element: any): element is SCMHistoryItemChangeViewModelTreeElement { + return (element as SCMHistoryItemChangeViewModelTreeElement).type === 'historyItemChangeViewModel'; +} + const compareActions = (a: IAction, b: IAction) => { if (a instanceof MenuItemAction && b instanceof MenuItemAction) { return a.id === b.id && a.enabled === b.enabled && a.hideActions?.isHidden === b.hideActions?.isHidden; diff --git a/src/vs/workbench/contrib/scm/common/history.ts b/src/vs/workbench/contrib/scm/common/history.ts index 3e9846603f3..c362c8ab3bd 100644 --- a/src/vs/workbench/contrib/scm/common/history.ts +++ b/src/vs/workbench/contrib/scm/common/history.ts @@ -88,6 +88,14 @@ export interface SCMHistoryItemViewModelTreeElement { readonly type: 'historyItemViewModel'; } +export interface SCMHistoryItemChangeViewModelTreeElement { + readonly repository: ISCMRepository; + readonly historyItemViewModel: ISCMHistoryItemViewModel; + readonly historyItemChange: ISCMHistoryItemChange; + readonly graphColumns: ISCMHistoryItemGraphNode[]; + readonly type: 'historyItemChangeViewModel'; +} + export interface SCMHistoryItemLoadMoreTreeElement { readonly repository: ISCMRepository; readonly graphColumns: ISCMHistoryItemGraphNode[]; diff --git a/src/vs/workbench/contrib/search/browser/chatContributions.ts b/src/vs/workbench/contrib/search/browser/chatContributions.ts new file mode 100644 index 00000000000..4d58715e25b --- /dev/null +++ b/src/vs/workbench/contrib/search/browser/chatContributions.ts @@ -0,0 +1,348 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Codicon } from '../../../../base/common/codicons.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { ThemeIcon } from '../../../../base/common/themables.js'; +import { localize } from '../../../../nls.js'; +import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { ILabelService } from '../../../../platform/label/common/label.js'; +import { IWorkbenchContribution } from '../../../common/contributions.js'; +import { getExcludes, IFileQuery, ISearchComplete, ISearchConfiguration, ISearchService, QueryType, VIEW_ID } from '../../../services/search/common/search.js'; +import { IViewsService } from '../../../services/views/common/viewsService.js'; +import { IChatContextPickerItem, IChatContextPickerPickItem, IChatContextPickService, IChatContextValueItem } from '../../chat/browser/chatContextPickService.js'; +import { IChatRequestVariableEntry, ISymbolVariableEntry } from '../../chat/common/chatModel.js'; +import { SearchContext } from '../common/constants.js'; +import { SearchView } from './searchView.js'; +import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { basename, dirname, joinPath, relativePath } from '../../../../base/common/resources.js'; +import { compare } from '../../../../base/common/strings.js'; +import { URI } from '../../../../base/common/uri.js'; +import { ILanguageService } from '../../../../editor/common/languages/language.js'; +import { getIconClasses } from '../../../../editor/common/services/getIconClasses.js'; +import { IModelService } from '../../../../editor/common/services/model.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { FileKind, FileType, IFileService } from '../../../../platform/files/common/files.js'; +import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; +import { IHistoryService } from '../../../services/history/common/history.js'; +import { isCancellationError } from '../../../../base/common/errors.js'; +import * as glob from '../../../../base/common/glob.js'; +import { ResourceSet } from '../../../../base/common/map.js'; +import { SymbolsQuickAccessProvider } from './symbolsQuickAccess.js'; +import { SymbolKinds } from '../../../../editor/common/languages.js'; + +export class SearchChatContextContribution extends Disposable implements IWorkbenchContribution { + + static readonly ID = 'workbench.contributions.searchChatContextContribution'; + + constructor( + @IInstantiationService instantiationService: IInstantiationService, + @IChatContextPickService chatContextPickService: IChatContextPickService + ) { + super(); + this._store.add(chatContextPickService.registerChatContextItem(instantiationService.createInstance(SearchViewResultChatContextPick))); + this._store.add(chatContextPickService.registerChatContextItem(instantiationService.createInstance(FilesAndFoldersPickerPick))); + this._store.add(chatContextPickService.registerChatContextItem(this._store.add(instantiationService.createInstance(SymbolsContextPickerPick)))); + } +} + +class SearchViewResultChatContextPick implements IChatContextValueItem { + + readonly type = 'valuePick'; + readonly label: string = localize('chatContext.searchResults', 'Search Results'); + readonly icon: ThemeIcon = Codicon.search; + readonly ordinal = 500; + + constructor( + @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @IViewsService private readonly _viewsService: IViewsService, + @ILabelService private readonly _labelService: ILabelService, + ) { } + + isEnabled(): Promise | boolean { + return !!SearchContext.HasSearchResults.getValue(this._contextKeyService); + } + + async asAttachment(): Promise { + const searchView = this._viewsService.getViewWithId(VIEW_ID); + if (!(searchView instanceof SearchView)) { + return []; + } + + return searchView.model.searchResult.matches().map(result => ({ + kind: 'file', + id: result.resource.toString(), + value: result.resource, + name: this._labelService.getUriBasenameLabel(result.resource), + })); + } +} + +class SymbolsContextPickerPick implements IChatContextPickerItem { + + readonly type = 'pickerPick'; + + readonly label: string = localize('symbols', 'Symbols...'); + readonly icon: ThemeIcon = Codicon.symbolField; + readonly ordinal = -200; + + private _provider: SymbolsQuickAccessProvider | undefined; + + constructor( + @IInstantiationService private readonly _instantiationService: IInstantiationService, + ) { } + + dispose(): void { + this._provider?.dispose(); + } + + asPicker(): { + readonly placeholder: string; + readonly picks: (query: string, token: CancellationToken) => Promise; + } { + + return { + placeholder: localize('select.symb', "Select a symbol"), + picks: async (query: string, token: CancellationToken) => { + + this._provider ??= this._instantiationService.createInstance(SymbolsQuickAccessProvider); + + return this._provider.getSymbolPicks(query, undefined, token).then(symbolItems => { + const result: IChatContextPickerPickItem[] = []; + for (const item of symbolItems) { + if (!item.symbol) { + continue; + } + + const attachment: ISymbolVariableEntry = { + kind: 'symbol', + id: JSON.stringify(item.symbol.location), + value: item.symbol.location, + symbolKind: item.symbol.kind, + icon: SymbolKinds.toIcon(item.symbol.kind), + fullName: item.label, + name: item.symbol.name, + }; + + result.push({ + label: item.symbol.name, + iconClass: ThemeIcon.asClassName(SymbolKinds.toIcon(item.symbol.kind)), + asAttachment() { + return attachment; + } + }); + } + return result; + }); + } + }; + } +} + +class FilesAndFoldersPickerPick implements IChatContextPickerItem { + + readonly type = 'pickerPick'; + readonly label = localize('chatContext.folder', 'Files & Folders...'); + readonly icon = Codicon.folder; + readonly ordinal = 600; + + constructor( + @ISearchService private readonly _searchService: ISearchService, + @ILabelService private readonly _labelService: ILabelService, + @IModelService private readonly _modelService: IModelService, + @ILanguageService private readonly _languageService: ILanguageService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + @IWorkspaceContextService private readonly _workspaceService: IWorkspaceContextService, + @IFileService private readonly _fileService: IFileService, + @IHistoryService private readonly _historyService: IHistoryService, + ) { } + + asPicker(): { + readonly placeholder: string; + readonly picks: (query: string, token: CancellationToken) => Promise; + } { + + return { + placeholder: localize('chatContext.attach.files.placeholder', "Search file or folder by name"), + picks: async (value, token) => { + + const workspaces = this._workspaceService.getWorkspace().folders.map(folder => folder.uri); + + const defaultItems: IChatContextPickerPickItem[] = []; + (await getTopLevelFolders(workspaces, this._fileService)).forEach(uri => defaultItems.push(this._createPickItem(uri, FileKind.FOLDER))); + this._historyService.getHistory().filter(a => a.resource).slice(0, 30).forEach(uri => defaultItems.push(this._createPickItem(uri.resource!, FileKind.FILE))); + + if (value === '') { + return defaultItems; + } + + const result: IChatContextPickerPickItem[] = []; + + await Promise.all(workspaces.map(async workspace => { + const { folders, files } = await searchFilesAndFolders( + workspace, + value, + true, + token, + undefined, + this._configurationService, + this._searchService + ); + + for (const folder of folders) { + result.push(this._createPickItem(folder, FileKind.FOLDER)); + } + for (const file of files) { + result.push(this._createPickItem(file, FileKind.FILE)); + } + })); + + result.sort((a, b) => compare(a.label, b.label)); + + return result; + }, + }; + } + + private _createPickItem(resource: URI, kind: FileKind): IChatContextPickerPickItem { + return { + label: basename(resource), + description: this._labelService.getUriLabel(dirname(resource), { relative: true }), + iconClass: kind === FileKind.FILE + ? getIconClasses(this._modelService, this._languageService, resource, FileKind.FILE).join(' ') + : ThemeIcon.asClassName(Codicon.folder), + asAttachment: () => { + return { + kind: kind === FileKind.FILE ? 'file' : 'directory', + id: resource.toString(), + value: resource, + name: basename(resource), + }; + } + }; + } + +} +export async function searchFilesAndFolders( + workspace: URI, + pattern: string, + fuzzyMatch: boolean, + token: CancellationToken | undefined, + cacheKey: string | undefined, + configurationService: IConfigurationService, + searchService: ISearchService +): Promise<{ folders: URI[]; files: URI[] }> { + const segmentMatchPattern = caseInsensitiveGlobPattern(fuzzyMatch ? fuzzyMatchingGlobPattern(pattern) : continousMatchingGlobPattern(pattern)); + + const searchExcludePattern = getExcludes(configurationService.getValue({ resource: workspace })) || {}; + const searchOptions: IFileQuery = { + folderQueries: [{ + folder: workspace, + disregardIgnoreFiles: configurationService.getValue('explorer.excludeGitIgnore'), + }], + type: QueryType.File, + shouldGlobMatchFilePattern: true, + cacheKey, + excludePattern: searchExcludePattern, + sortByScore: true, + }; + + let searchResult: ISearchComplete | undefined; + try { + searchResult = await searchService.fileSearch({ ...searchOptions, filePattern: `{**/${segmentMatchPattern}/**,${pattern}}` }, token); + } catch (e) { + if (!isCancellationError(e)) { + throw e; + } + } + + if (!searchResult || token?.isCancellationRequested) { + return { files: [], folders: [] }; + } + + const fileResources = searchResult.results.map(result => result.resource); + const folderResources = getMatchingFoldersFromFiles(fileResources, workspace, segmentMatchPattern); + + return { folders: folderResources, files: fileResources }; +} + +function fuzzyMatchingGlobPattern(pattern: string): string { + if (!pattern) { + return '*'; + } + return '*' + pattern.split('').join('*') + '*'; +} + +function continousMatchingGlobPattern(pattern: string): string { + if (!pattern) { + return '*'; + } + return '*' + pattern + '*'; +} + +function caseInsensitiveGlobPattern(pattern: string): string { + let caseInsensitiveFilePattern = ''; + for (let i = 0; i < pattern.length; i++) { + const char = pattern[i]; + if (/[a-zA-Z]/.test(char)) { + caseInsensitiveFilePattern += `[${char.toLowerCase()}${char.toUpperCase()}]`; + } else { + caseInsensitiveFilePattern += char; + } + } + return caseInsensitiveFilePattern; +} + +// TODO: remove this and have support from the search service +function getMatchingFoldersFromFiles(resources: URI[], workspace: URI, segmentMatchPattern: string): URI[] { + const uniqueFolders = new ResourceSet(); + for (const resource of resources) { + const relativePathToRoot = relativePath(workspace, resource); + if (!relativePathToRoot) { + throw new Error('Resource is not a child of the workspace'); + } + + let dirResource = workspace; + const stats = relativePathToRoot.split('/').slice(0, -1); + for (const stat of stats) { + dirResource = dirResource.with({ path: `${dirResource.path}/${stat}` }); + uniqueFolders.add(dirResource); + } + } + + const matchingFolders: URI[] = []; + for (const folderResource of uniqueFolders) { + const stats = folderResource.path.split('/'); + const dirStat = stats[stats.length - 1]; + if (!dirStat || !glob.match(segmentMatchPattern, dirStat)) { + continue; + } + + matchingFolders.push(folderResource); + } + + return matchingFolders; +} + +export async function getTopLevelFolders(workspaces: URI[], fileService: IFileService): Promise { + const folders: URI[] = []; + for (const workspace of workspaces) { + const fileSystemProvider = fileService.getProvider(workspace.scheme); + if (!fileSystemProvider) { + continue; + } + + const entries = await fileSystemProvider.readdir(workspace); + for (const [name, type] of entries) { + const entryResource = joinPath(workspace, name); + if (type === FileType.Directory) { + folders.push(entryResource); + } + } + } + + return folders; +} diff --git a/src/vs/workbench/contrib/search/browser/search.contribution.ts b/src/vs/workbench/contrib/search/browser/search.contribution.ts index 1775ae2cd21..223ce5f9541 100644 --- a/src/vs/workbench/contrib/search/browser/search.contribution.ts +++ b/src/vs/workbench/contrib/search/browser/search.contribution.ts @@ -27,11 +27,12 @@ import { SymbolsQuickAccessProvider } from './symbolsQuickAccess.js'; import { ISearchHistoryService, SearchHistoryService } from '../common/searchHistoryService.js'; import { SearchViewModelWorkbenchService } from './searchTreeModel/searchModel.js'; import { ISearchViewModelWorkbenchService } from './searchTreeModel/searchViewModelWorkbenchService.js'; -import { SearchSortOrder, SEARCH_EXCLUDE_CONFIG, VIEWLET_ID, ViewMode, VIEW_ID, DEFAULT_MAX_SEARCH_RESULTS } from '../../../services/search/common/search.js'; +import { SearchSortOrder, SEARCH_EXCLUDE_CONFIG, VIEWLET_ID, ViewMode, VIEW_ID, DEFAULT_MAX_SEARCH_RESULTS, SemanticSearchBehavior } from '../../../services/search/common/search.js'; import { CommandsRegistry } from '../../../../platform/commands/common/commands.js'; import { assertType } from '../../../../base/common/types.js'; import { getWorkspaceSymbols, IWorkspaceSymbol } from '../common/search.js'; import * as Constants from '../common/constants.js'; +import { SearchChatContextContribution } from './chatContributions.js'; import './searchActionsCopy.js'; import './searchActionsFind.js'; @@ -42,6 +43,7 @@ import './searchActionsTopBar.js'; import './searchActionsTextQuickAccess.js'; import { TEXT_SEARCH_QUICK_ACCESS_PREFIX, TextSearchQuickAccess } from './quickTextSearch/textSearchQuickAccess.js'; import { Extensions, IConfigurationMigrationRegistry } from '../../../common/configuration.js'; +import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; registerSingleton(ISearchViewModelWorkbenchService, SearchViewModelWorkbenchService, InstantiationType.Delayed); registerSingleton(ISearchHistoryService, SearchHistoryService, InstantiationType.Delayed); @@ -50,6 +52,8 @@ replaceContributions(); notebookSearchContributions(); searchWidgetContributions(); +registerWorkbenchContribution2(SearchChatContextContribution.ID, SearchChatContextContribution, WorkbenchPhase.AfterRestored); + const SEARCH_MODE_CONFIG = 'search.mode'; const viewContainer = Registry.as(ViewExtensions.ViewContainersRegistry).registerViewContainer({ @@ -384,7 +388,24 @@ configurationRegistry.registerConfiguration({ description: nls.localize('search.experimental.closedNotebookResults', "Show notebook editor rich content results for closed notebooks. Please refresh your search results after changing this setting."), default: false }, - + 'search.searchView.semanticSearchBehavior': { + type: 'string', + description: nls.localize('search.searchView.semanticSearchBehavior', "Controls the behavior of the semantic search results displayed in the search view."), + enum: [SemanticSearchBehavior.Manual, SemanticSearchBehavior.RunOnEmpty, SemanticSearchBehavior.Auto], + default: SemanticSearchBehavior.RunOnEmpty, + enumDescriptions: [ + nls.localize('search.searchView.semanticSearchBehavior.manual', "Only request semantic search results manually."), + nls.localize('search.searchView.semanticSearchBehavior.runOnEmpty', "Request semantic results automatically only when text search results are empty."), + nls.localize('search.searchView.semanticSearchBehavior.auto', "Request semantic results automatically with every search.") + ], + tags: ['preview'], + }, + 'search.searchView.keywordSuggestions': { + type: 'boolean', + description: nls.localize('search.searchView.keywordSuggestions', "Enable keyword suggestions in the search view."), + default: false, + tags: ['preview'], + }, } }); diff --git a/src/vs/workbench/contrib/search/browser/searchActionsRemoveReplace.ts b/src/vs/workbench/contrib/search/browser/searchActionsRemoveReplace.ts index 54d7b8f24d8..e25bda863cf 100644 --- a/src/vs/workbench/contrib/search/browser/searchActionsRemoveReplace.ts +++ b/src/vs/workbench/contrib/search/browser/searchActionsRemoveReplace.ts @@ -129,7 +129,8 @@ registerAction2(class RemoveAction extends Action2 { if (focusElement && shouldRefocusMatch) { if (!nextFocusElement) { - nextFocusElement = await getLastNodeFromSameType(viewer, focusElement); + // Ignore error if there are no elements left + nextFocusElement = await getLastNodeFromSameType(viewer, focusElement).catch(() => { }); } if (nextFocusElement && !arrayContainsElementOrParent(nextFocusElement, elementsToRemove)) { diff --git a/src/vs/workbench/contrib/search/browser/searchActionsTopBar.ts b/src/vs/workbench/contrib/search/browser/searchActionsTopBar.ts index 7a0f2a2152b..ce60af79ee4 100644 --- a/src/vs/workbench/contrib/search/browser/searchActionsTopBar.ts +++ b/src/vs/workbench/contrib/search/browser/searchActionsTopBar.ts @@ -224,12 +224,7 @@ registerAction2(class SearchWithAIAction extends Action2 { async run(accessor: ServicesAccessor, ...args: any[]) { const searchView = getSearchView(accessor.get(IViewsService)); if (searchView) { - const viewer = searchView.getControl(); - searchView.model.searchResult.aiTextSearchResult.hidden = false; - searchView.model.cancelAISearch(true); - searchView.model.clearAiSearchResults(); - await searchView.queueRefreshTree(); - await forcedExpandRecursively(viewer, searchView.model.searchResult.aiTextSearchResult); + searchView.requestAIResults(); } } }); @@ -247,16 +242,7 @@ async function expandAll(accessor: ServicesAccessor) { const searchView = getSearchView(viewsService); if (searchView) { const viewer = searchView.getControl(); - - if (searchView.shouldShowAIResults()) { - if (searchView.model.hasAIResults) { - await forcedExpandRecursively(viewer, undefined); - } else { - await forcedExpandRecursively(viewer, searchView.model.searchResult.plainTextSearchResult); - } - } else { - await forcedExpandRecursively(viewer, undefined); - } + await forcedExpandRecursively(viewer, undefined); } } diff --git a/src/vs/workbench/contrib/search/browser/searchTreeModel/searchModel.ts b/src/vs/workbench/contrib/search/browser/searchTreeModel/searchModel.ts index bb17f12e972..10b15289316 100644 --- a/src/vs/workbench/contrib/search/browser/searchTreeModel/searchModel.ts +++ b/src/vs/workbench/contrib/search/browser/searchTreeModel/searchModel.ts @@ -359,7 +359,7 @@ export class SearchModelImpl extends Disposable implements ISearchModel { } else { this._startStreamDelay.then(() => { if (targetQueue.length) { - this._searchResult.add(targetQueue, searchInstanceID, ai, false); + this._searchResult.add(targetQueue, searchInstanceID, ai, !ai); targetQueue.length = 0; } }); diff --git a/src/vs/workbench/contrib/search/browser/searchTreeModel/searchResult.ts b/src/vs/workbench/contrib/search/browser/searchTreeModel/searchResult.ts index f2e0fa7ee3b..e355277b772 100644 --- a/src/vs/workbench/contrib/search/browser/searchTreeModel/searchResult.ts +++ b/src/vs/workbench/contrib/search/browser/searchTreeModel/searchResult.ts @@ -197,9 +197,6 @@ export class SearchResultImpl extends Disposable implements ISearchResult { add(allRaw: IFileMatch[], searchInstanceID: string, ai: boolean, silent: boolean = false): void { this._plainTextSearchResult.hidden = false; - if (ai) { - this._aiTextSearchResult.hidden = false; - } if (ai) { this._aiTextSearchResult.add(allRaw, searchInstanceID, silent); diff --git a/src/vs/workbench/contrib/search/browser/searchView.ts b/src/vs/workbench/contrib/search/browser/searchView.ts index c8336b641aa..c46c00b99f7 100644 --- a/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/src/vs/workbench/contrib/search/browser/searchView.ts @@ -14,7 +14,6 @@ import * as errors from '../../../../base/common/errors.js'; import { Event } from '../../../../base/common/event.js'; import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; import { Disposable, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js'; -import * as env from '../../../../base/common/platform.js'; import * as strings from '../../../../base/common/strings.js'; import { URI } from '../../../../base/common/uri.js'; import * as network from '../../../../base/common/network.js'; @@ -48,7 +47,7 @@ import { defaultInputBoxStyles, defaultToggleStyles } from '../../../../platform import { IFileIconTheme, IThemeService } from '../../../../platform/theme/common/themeService.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { IWorkspaceContextService, WorkbenchState } from '../../../../platform/workspace/common/workspace.js'; -import { OpenFileFolderAction, OpenFolderAction } from '../../../browser/actions/workspaceActions.js'; +import { OpenFolderAction } from '../../../browser/actions/workspaceActions.js'; import { ResourceListDnDHandler } from '../../../browser/dnd.js'; import { ResourceLabels } from '../../../browser/labels.js'; import { IViewPaneOptions, ViewPane } from '../../../browser/parts/views/viewPane.js'; @@ -71,7 +70,7 @@ import { createEditorFromSearchResult } from '../../searchEditor/browser/searchE import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from '../../../services/editor/common/editorService.js'; import { IPreferencesService, ISettingsEditorOptions } from '../../../services/preferences/common/preferences.js'; import { ITextQueryBuilderOptions, QueryBuilder } from '../../../services/search/common/queryBuilder.js'; -import { IPatternInfo, ISearchComplete, ISearchConfiguration, ISearchConfigurationProperties, ISearchService, ITextQuery, SearchCompletionExitCode, SearchSortOrder, TextSearchCompleteMessageType, ViewMode } from '../../../services/search/common/search.js'; +import { SemanticSearchBehavior, IPatternInfo, ISearchComplete, ISearchConfiguration, ISearchConfigurationProperties, ISearchService, ITextQuery, SearchCompletionExitCode, SearchSortOrder, TextSearchCompleteMessageType, ViewMode } from '../../../services/search/common/search.js'; import { AISearchKeyword, TextSearchCompleteMessage } from '../../../services/search/common/searchExtTypes.js'; import { ITextFileService } from '../../../services/textfile/common/textfiles.js'; import { INotebookService } from '../../notebook/common/notebookService.js'; @@ -85,6 +84,7 @@ import { INotebookFileInstanceMatch, isIMatchInNotebook } from './notebookSearch import { searchMatchComparer } from './searchCompare.js'; import { AIFolderMatchWorkspaceRootImpl } from './AISearch/aiSearchModel.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; +import { forcedExpandRecursively } from './searchActionsTopBar.js'; const $ = dom.$; @@ -172,6 +172,8 @@ export class SearchView extends ViewPane { private searchDataSource: SearchViewDataSource | undefined; private refreshTreeController: RefreshTreeController; + + private _cachedResults: ISearchComplete | undefined; constructor( options: IViewPaneOptions, @IFileService private readonly fileService: IFileService, @@ -306,6 +308,10 @@ export class SearchView extends ViewPane { this.changedWhileHidden = this.hasSearchResults(); } + public get cachedResults() { + return this._cachedResults; + } + async queueRefreshTree(): Promise { return this.refreshTreeController.queue(); } @@ -628,7 +634,14 @@ export class SearchView extends ViewPane { this.searchWidget.toggleReplace(true); } - this._register(this.searchWidget.onSearchSubmit(options => this.triggerQueryChange(options))); + this._register(this.searchWidget.onSearchSubmit(options => { + const shouldRenderAIResults = this.configurationService.getValue('search').searchView.semanticSearchBehavior; + this.triggerQueryChange({ + ...options, + shouldKeepAIResults: false, + shouldUpdateAISearch: shouldRenderAIResults === SemanticSearchBehavior.Auto, + }); + })); this._register(this.searchWidget.onSearchCancel(({ focus }) => this.cancelSearch(focus))); this._register(this.searchWidget.searchInput.onDidOptionChange(() => { this.triggerQueryChange({ shouldKeepAIResults: true }); @@ -1526,6 +1539,7 @@ export class SearchView extends ViewPane { if (contentPattern.length === 0) { this.clearSearchResults(false); this.clearMessage(); + this.clearAIResults(); return; } @@ -1582,8 +1596,7 @@ export class SearchView extends ViewPane { } this.validateQuery(query).then(() => { - // ensure that the node is closed when a new search is triggered - if (!shouldKeepAIResults && !shouldUpdateAISearch && this.tree.hasNode(this.searchResult.aiTextSearchResult)) { + if (!shouldKeepAIResults && shouldUpdateAISearch && this.tree.hasNode(this.searchResult.aiTextSearchResult)) { this.tree.collapse(this.searchResult.aiTextSearchResult); } @@ -1624,8 +1637,7 @@ export class SearchView extends ViewPane { this.inputPatternIncludes.onSearchSubmit(); }); - this.viewModel.cancelSearch(true); - this.viewModel.cancelAISearch(true); + this.clearAIResults(); this.currentSearchQ = this.currentSearchQ .then(() => this.doSearch(query, excludePatternText, includePatternText, triggeredOnType, shouldKeepAIResults, shouldUpdateAISearch)) @@ -1663,7 +1675,14 @@ export class SearchView extends ViewPane { } } - private async onSearchComplete(progressComplete: () => void, excludePatternText?: string, includePatternText?: string, completed?: ISearchComplete, shouldDoFinalRefresh = true) { + private async onSearchComplete( + progressComplete: () => void, + excludePatternText?: string, + includePatternText?: string, + completed?: ISearchComplete, + shouldDoFinalRefresh = true, + keywords?: AISearchKeyword[], + ) { this.state = SearchUIState.Idle; @@ -1684,6 +1703,16 @@ export class SearchView extends ViewPane { // Special case for when we have an AI provider registered Constants.SearchContext.AIResultsRequested.bindTo(this.contextKeyService).set(this.shouldShowAIResults() && !!aiResults); + // Expand AI results if the node is collapsed + if (completed && this.tree.hasNode(this.searchResult.aiTextSearchResult) && this.tree.isCollapsed(this.searchResult.aiTextSearchResult)) { + this.tree.expand(this.searchResult.aiTextSearchResult); + return; + } + + if (this.configurationService.getValue('search').searchView.keywordSuggestions) { + this.updateKeywordSuggestion(keywords); + } + if (this.shouldShowAIResults() && !allResults) { const messageEl = this.clearMessage(); const noResultsMessage = nls.localize('noResultsFallback', "No results found. "); @@ -1808,24 +1837,28 @@ export class SearchView extends ViewPane { } } - public async addAIResults(element: ITextSearchHeading, createIterator: (e: ITextSearchHeading) => Iterable) { + public clearAIResults() { + this._cachedResults = undefined; + this.model.cancelAISearch(true); + this.model.clearAiSearchResults(); + } + + public async requestAIResults() { + if (!this.cachedResults) { + this.clearAIResults(); + } + this.model.searchResult.aiTextSearchResult.hidden = false; + await this.queueRefreshTree(); + await forcedExpandRecursively(this.getControl(), this.model.searchResult.aiTextSearchResult); + } + + public async addAIResults() { const excludePatternText = this._getExcludePattern(); const includePatternText = this._getIncludePattern(); - let progressComplete: () => void; - this.progressService.withProgress({ location: this.getProgressLocation(), delay: 0 }, _progress => { - return new Promise(resolve => progressComplete = resolve); - }); this.searchWidget.searchInput?.clearMessage(); - this.state = SearchUIState.Searching; this.showEmptyStage(); - - const slowTimer = setTimeout(() => { - this.state = SearchUIState.SlowSearch; - }, 2000); - this._visibleMatches = 0; - this.tree.setSelection([]); this.tree.setFocus([]); @@ -1833,22 +1866,10 @@ export class SearchView extends ViewPane { this.viewModel.searchResult.setAIQueryUsingTextQuery(); const result = this.viewModel.aiSearch(); result.then((complete) => { - clearTimeout(slowTimer); - if (complete.aiKeywords && complete.aiKeywords.length > 0) { - this.updateKeywordSuggestion(complete.aiKeywords); - } else { - this.updateSearchResultCount(this.viewModel.searchResult.query?.userDisabledExcludesAndIgnoreFiles, this.viewModel.searchResult.query?.onlyOpenEditors, false); - } - return this.onSearchComplete(progressComplete, excludePatternText, includePatternText, complete, false); + this.updateSearchResultCount(this.viewModel.searchResult.query?.userDisabledExcludesAndIgnoreFiles, this.viewModel.searchResult.query?.onlyOpenEditors, false); + return this.onSearchComplete(() => { }, excludePatternText, includePatternText, complete, false, complete.aiKeywords); }, (e) => { - clearTimeout(slowTimer); - return this.onSearchError(e, progressComplete, excludePatternText, includePatternText, undefined, false); - }); - return new Promise>(resolve => { - const disposable = element.onChange(() => { - disposable.dispose(); // Clean up listener after first result - resolve(createIterator(element)); - }); + return this.onSearchError(e, () => { }, excludePatternText, includePatternText, undefined, false); }); } @@ -1883,12 +1904,12 @@ export class SearchView extends ViewPane { this.viewModel.searchResult.setAIQueryUsingTextQuery(query); } - if (shouldUpdateAISearch) { - this.tree.updateChildren(this.searchResult.aiTextSearchResult); - } - return result.asyncResults.then((complete) => { clearTimeout(slowTimer); + const config = this.configurationService.getValue('search').searchView.semanticSearchBehavior; + if (complete.results.length === 0 && config === SemanticSearchBehavior.RunOnEmpty) { + this.model.searchResult.aiTextSearchResult.hidden = false; + } return this.onSearchComplete(progressComplete, excludePatternText, includePatternText, complete); }, (e) => { clearTimeout(slowTimer); @@ -1991,7 +2012,16 @@ export class SearchView extends ViewPane { }); } - private updateKeywordSuggestion(keywords: AISearchKeyword[]) { + private async updateKeywordSuggestion(keywords?: AISearchKeyword[]) { + if (!keywords || keywords.length === 0) { + this.viewModel.replaceString = this.searchWidget.getReplaceValue(); + this.viewModel.searchResult.setAIQueryUsingTextQuery(); + this._cachedResults = await this.viewModel.aiSearch(); + keywords = this._cachedResults.aiKeywords; + if (!keywords || keywords.length === 0) { + return; + } + } const messageEl = this.clearMessage(); messageEl.classList.add('ai-keywords'); @@ -2045,7 +2075,7 @@ export class SearchView extends ViewPane { const openFolderButton = this.messageDisposables.add(new SearchLinkButton( nls.localize('openFolder', "Open Folder"), () => { - this.commandService.executeCommand(env.isMacintosh && env.isNative ? OpenFileFolderAction.ID : OpenFolderAction.ID).catch(err => errors.onUnexpectedError(err)); + this.commandService.executeCommand(OpenFolderAction.ID).catch(err => errors.onUnexpectedError(err)); }, this.hoverService)); dom.append(textEl, openFolderButton.element); } @@ -2510,7 +2540,16 @@ class SearchViewDataSource implements IAsyncDataSource this.createTextSearchResultIterator(e)); + if (this.searchView.cachedResults) { + return this.createTextSearchResultIterator(element); + } + this.searchView.addAIResults(); + return new Promise>(resolve => { + const disposable = element.onChange(() => { + disposable.dispose(); // Clean up listener after first result + resolve(this.createTextSearchResultIterator(element)); + }); + }); } return this.createTextSearchResultIterator(element); } else if (isSearchTreeFolderMatch(element)) { diff --git a/src/vs/workbench/contrib/search/common/cacheState.ts b/src/vs/workbench/contrib/search/common/cacheState.ts index 3d7aa51da36..9090835bda7 100644 --- a/src/vs/workbench/contrib/search/common/cacheState.ts +++ b/src/vs/workbench/contrib/search/common/cacheState.ts @@ -17,7 +17,7 @@ enum LoadingPhase { export class FileQueryCacheState { - private readonly _cacheKey = defaultGenerator.nextId(); + private readonly _cacheKey; get cacheKey(): string { if (this.loadingPhase === LoadingPhase.Loaded || !this.previousCacheState) { return this._cacheKey; @@ -38,9 +38,9 @@ export class FileQueryCacheState { return isUpdating || !this.previousCacheState ? isUpdating : this.previousCacheState.isUpdating; } - private readonly query = this.cacheQuery(this._cacheKey); + private readonly query; - private loadingPhase = LoadingPhase.Created; + private loadingPhase; private loadPromise: Promise | undefined; constructor( @@ -49,6 +49,9 @@ export class FileQueryCacheState { private disposeFn: (cacheKey: string) => Promise, private previousCacheState: FileQueryCacheState | undefined ) { + this._cacheKey = defaultGenerator.nextId(); + this.query = this.cacheQuery(this._cacheKey); + this.loadingPhase = LoadingPhase.Created; if (this.previousCacheState) { const current = Object.assign({}, this.query, { cacheKey: null }); const previous = Object.assign({}, this.previousCacheState.query, { cacheKey: null }); diff --git a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts index 78c67ee4a15..3205ac97777 100644 --- a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts @@ -47,7 +47,7 @@ import { ITextFileService } from '../../../services/textfile/common/textfiles.js import { ITerminalGroupService, ITerminalService } from '../../terminal/browser/terminal.js'; import { ITerminalProfileResolverService } from '../../terminal/common/terminal.js'; -import { ConfiguringTask, ContributedTask, CustomTask, ExecutionEngine, InMemoryTask, ITaskEvent, ITaskIdentifier, ITaskSet, JsonSchemaVersion, KeyedTaskIdentifier, RuntimeType, Task, TASK_RUNNING_STATE, TaskDefinition, TaskGroup, TaskRunSource, TaskSettingId, TaskSorter, TaskSourceKind, TasksSchemaProperties, USER_TASKS_GROUP_KEY, TaskEventKind } from '../common/tasks.js'; +import { ConfiguringTask, ContributedTask, CustomTask, ExecutionEngine, InMemoryTask, ITaskEvent, ITaskIdentifier, ITaskSet, JsonSchemaVersion, KeyedTaskIdentifier, RuntimeType, Task, TASK_RUNNING_STATE, TaskDefinition, TaskGroup, TaskRunSource, TaskSettingId, TaskSorter, TaskSourceKind, TasksSchemaProperties, USER_TASKS_GROUP_KEY, TaskEventKind, InstancePolicy } from '../common/tasks.js'; import { CustomExecutionSupportedContext, ICustomizationProperties, IProblemMatcherRunOptions, ITaskFilter, ITaskProvider, ITaskService, IWorkspaceFolderTaskResult, ProcessExecutionSupportedContext, ServerlessWebContext, ShellExecutionSupportedContext, TaskCommandsRegistered, TaskExecutionSupportedContext } from '../common/taskService.js'; import { ITaskExecuteResult, ITaskResolver, ITaskSummary, ITaskSystem, ITaskSystemInfo, ITaskTerminateResponse, TaskError, TaskErrors, TaskExecuteKind } from '../common/taskSystem.js'; import { getTemplates as getTaskTemplates } from '../common/taskTemplates.js'; @@ -84,6 +84,9 @@ import { IPathService } from '../../../services/path/common/pathService.js'; import { IPreferencesService } from '../../../services/preferences/common/preferences.js'; import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js'; import { isCancellationError } from '../../../../base/common/errors.js'; +import { IChatService } from '../../chat/common/chatService.js'; +import { ChatAgentLocation, ChatMode } from '../../chat/common/constants.js'; +import { CHAT_OPEN_ACTION_ID } from '../../chat/browser/actions/chatActions.js'; const QUICKOPEN_HISTORY_LIMIT_CONFIG = 'task.quickOpen.history'; @@ -100,6 +103,8 @@ export type TaskQuickPickEntryType = (IQuickPickItem & { task: Task }) | (IQuick class ProblemReporter implements TaskConfig.IProblemReporter { private _validationStatus: ValidationStatus; + private readonly _onDidError: Emitter = new Emitter(); + public readonly onDidError: Event = this._onDidError.event; constructor(private _outputChannel: IOutputChannel) { this._validationStatus = new ValidationStatus(); @@ -118,11 +123,13 @@ class ProblemReporter implements TaskConfig.IProblemReporter { public error(message: string): void { this._validationStatus.state = ValidationState.Error; this._outputChannel.append(message + '\n'); + this._onDidError.fire(message); } public fatal(message: string): void { this._validationStatus.state = ValidationState.Fatal; this._outputChannel.append(message + '\n'); + this._onDidError.fire(message); } public get status(): ValidationStatus { @@ -277,7 +284,8 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer @IThemeService private readonly _themeService: IThemeService, @ILifecycleService private readonly _lifecycleService: ILifecycleService, @IRemoteAgentService remoteAgentService: IRemoteAgentService, - @IInstantiationService private readonly _instantiationService: IInstantiationService + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IChatService private readonly _chatService: IChatService, ) { super(); this._whenTaskSystemReady = Event.toPromise(this.onDidChangeTaskSystemInfo); @@ -660,18 +668,43 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer this._workspace = setup[4]; } - protected _showOutput(runSource: TaskRunSource = TaskRunSource.User, userRequested?: boolean): void { + protected _showOutput(runSource: TaskRunSource = TaskRunSource.User, userRequested?: boolean, errorMessage?: string): void { if (!VirtualWorkspaceContext.getValue(this._contextKeyService) && ((runSource === TaskRunSource.User) || (runSource === TaskRunSource.ConfigurationChange))) { if (userRequested) { this._outputService.showChannel(this._outputChannel.id, true); } else { - this._notificationService.prompt(Severity.Warning, nls.localize('taskServiceOutputPrompt', 'There are task errors. See the output for details.'), - [{ - label: nls.localize('showOutput', "Show output"), - run: () => { - this._outputService.showChannel(this._outputChannel.id, true); - } - }]); + const chatEnabled = this._chatService.isEnabled(ChatAgentLocation.Panel); + const actions = []; + if (chatEnabled && errorMessage) { + const beforeJSONregex = /^(.*?)\s*\{[\s\S]*$/; + const matches = errorMessage.match(beforeJSONregex); + if (matches && matches.length > 1) { + const message = matches[1]; + const customMessage = message === errorMessage + ? `\`${message}\`` + : `\`${message}\`\n\`\`\`json${errorMessage}\`\`\``; + actions.push({ + label: nls.localize('troubleshootWithChat', "Fix with Chat"), + run: async () => { + this._commandService.executeCommand(CHAT_OPEN_ACTION_ID, { + mode: ChatMode.Agent, + query: `Fix this task configuration error: ${customMessage}` + }); + } + }); + } + } + actions.push({ + label: nls.localize('showOutput', "Show Output"), + run: () => { + this._outputService.showChannel(this._outputChannel.id, true); + } + }); + if (chatEnabled) { + this._notificationService.prompt(Severity.Warning, nls.localize('taskServiceOutputPromptChat', 'There are task errors. Use chat to fix them or view the output for details.'), actions); + } else { + this._notificationService.prompt(Severity.Warning, nls.localize('taskServiceOutputPrompt', 'There are task errors. See the output for details.'), actions); + } } } } @@ -1942,23 +1975,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer return executeResult.promise; } if (active && active.same) { - if (this._taskSystem?.isTaskVisible(executeResult.task)) { - const message = nls.localize('TaskSystem.activeSame.noBackground', 'The task \'{0}\' is already active.', executeResult.task.getQualifiedLabel()); - const lastInstance = this._getTaskSystem().getLastInstance(executeResult.task) ?? executeResult.task; - this._notificationService.prompt(Severity.Warning, message, - [{ - label: nls.localize('terminateTask', "Terminate Task"), - run: () => this.terminate(lastInstance) - }, - { - label: nls.localize('restartTask', "Restart Task"), - run: () => this._restart(lastInstance) - }], - { sticky: true } - ); - } else { - this._taskSystem?.revealTask(executeResult.task); - } + this._handleInstancePolicy(executeResult.task, executeResult.task.runOptions!.instancePolicy); } else { throw new TaskError(Severity.Warning, nls.localize('TaskSystem.active', 'There is already a task running. Terminate it first before executing another task.'), TaskErrors.RunningTask); } @@ -1967,6 +1984,42 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer return executeResult.promise; } + private _handleInstancePolicy(task: Task, policy?: InstancePolicy): void { + if (!this._taskSystem?.isTaskVisible(task)) { + this._taskSystem?.revealTask(task); + } + switch (policy) { + case InstancePolicy.terminateNewest: + this._restart(this._getTaskSystem().getLastInstance(task) ?? task); + break; + case InstancePolicy.terminateOldest: + this._restart(this._getTaskSystem().getFirstInstance(task) ?? task); + break; + case InstancePolicy.silent: + break; + case InstancePolicy.warn: + this._notificationService.warn(nls.localize('TaskSystem.InstancePolicy.warn', 'The instance limit for this task has been reached.')); + break; + case InstancePolicy.prompt: + default: + this._showQuickPick(this._taskSystem!.getActiveTasks().filter(t => task._id === t._id), + nls.localize('TaskService.instanceToTerminate', 'Select an instance to terminate'), + { + label: nls.localize('TaskService.noInstanceRunning', 'No instance is currently running'), + task: undefined + }, + false, true, + undefined + ).then(entry => { + const task: Task | undefined | null = entry ? entry.task : undefined; + if (task === undefined || task === null) { + return; + } + this._restart(task); + }); + } + } + private async _restart(task: Task): Promise { if (!this._taskSystem) { return; @@ -2060,10 +2113,10 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer if (!isCancellationError(error)) { if (error && Types.isString(error.message)) { this._log(`Error: ${error.message}\n`); - this._showOutput(); + this._showOutput(error.message); } else { this._log('Unknown error received while collecting tasks from providers.'); - this._showOutput(); + this._showOutput(undefined, undefined, 'Unknown error received while collecting tasks from providers.'); } } } finally { @@ -2088,7 +2141,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer if (task.type !== this._providerTypes.get(handle)) { this._log(nls.localize('unexpectedTaskType', "The task provider for \"{0}\" tasks unexpectedly provided a task of type \"{1}\".\n", this._providerTypes.get(handle), task.type)); if ((task.type !== 'shell') && (task.type !== 'process')) { - this._showOutput(); + this._showOutput(undefined, undefined, nls.localize('unexpectedTaskType', "The task provider for \"{0}\" tasks unexpectedly provided a task of type \"{1}\".\n", this._providerTypes.get(handle), task.type)); } break; } @@ -2355,11 +2408,11 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer await ProblemMatcherRegistry.onReady(); const taskSystemInfo: ITaskSystemInfo | undefined = this._getTaskSystemInfo(workspaceFolder.uri.scheme); const problemReporter = new ProblemReporter(this._outputChannel); + this._register(problemReporter.onDidError(error => this._showOutput(runSource, undefined, error))); const parseResult = TaskConfig.parse(workspaceFolder, undefined, taskSystemInfo ? taskSystemInfo.platform : Platform.platform, workspaceFolderConfiguration.config, problemReporter, TaskConfig.TaskConfigSource.TasksJson, this._contextKeyService); let hasErrors = false; if (!parseResult.validationStatus.isOK() && (parseResult.validationStatus.state !== ValidationState.Info)) { hasErrors = true; - this._showOutput(runSource); } if (problemReporter.status.isFatal()) { problemReporter.fatal(nls.localize('TaskSystem.configurationErrors', 'Error: the provided task configuration has validation errors and can\'t not be used. Please correct the errors first.')); @@ -2395,7 +2448,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } if (isAffected) { this._log(nls.localize({ key: 'TaskSystem.invalidTaskJsonOther', comment: ['Message notifies of an error in one of several places there is tasks related json, not necessarily in a file named tasks.json'] }, 'Error: The content of the tasks json in {0} has syntax errors. Please correct them before executing a task.', location)); - this._showOutput(); + this._showOutput(undefined, undefined, nls.localize({ key: 'TaskSystem.invalidTaskJsonOther', comment: ['Message notifies of an error in one of several places there is tasks related json, not necessarily in a file named tasks.json'] }, 'Error: The content of the tasks json in {0} has syntax errors. Please correct them before executing a task.', location)); return { config, hasParseErrors: true }; } } @@ -2575,7 +2628,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } if (isAffected) { this._log(nls.localize('TaskSystem.invalidTaskJson', 'Error: The content of the tasks.json file has syntax errors. Please correct them before executing a task.')); - this._showOutput(); + this._showOutput(undefined, undefined, nls.localize('TaskSystem.invalidTaskJson', 'Error: The content of the tasks.json file has syntax errors. Please correct them before executing a task.')); return { config: undefined, hasParseErrors: true }; } } @@ -2628,7 +2681,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer this._notificationService.error(nls.localize('TaskSystem.unknownError', 'An error has occurred while running a task. See task log for details.')); } if (showOutput) { - this._showOutput(); + this._showOutput(undefined, undefined, err); } } diff --git a/src/vs/workbench/contrib/tasks/browser/task.contribution.ts b/src/vs/workbench/contrib/tasks/browser/task.contribution.ts index 8629aa38b0f..a9780480c6d 100644 --- a/src/vs/workbench/contrib/tasks/browser/task.contribution.ts +++ b/src/vs/workbench/contrib/tasks/browser/task.contribution.ts @@ -595,3 +595,4 @@ registerAction2(class extends Action2 { } } }); + diff --git a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts index ca3267564ca..a57b3ad711a 100644 --- a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts +++ b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts @@ -399,6 +399,16 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { (value) => recentKey && recentKey === value.task.getKey())?.task; } + public getFirstInstance(task: Task): Task | undefined { + const recentKey = task.getKey(); + for (const task of this.getActiveTasks()) { + if (recentKey && recentKey === task.getKey()) { + return task; + } + } + return undefined; + } + public getBusyTasks(): Task[] { return Object.keys(this._busyTasks).map(key => this._busyTasks[key]); } diff --git a/src/vs/workbench/contrib/tasks/common/jsonSchema_v2.ts b/src/vs/workbench/contrib/tasks/common/jsonSchema_v2.ts index 42df6dbc164..e698eb98988 100644 --- a/src/vs/workbench/contrib/tasks/common/jsonSchema_v2.ts +++ b/src/vs/workbench/contrib/tasks/common/jsonSchema_v2.ts @@ -378,6 +378,19 @@ const runOptions: IJSONSchema = { description: nls.localize('JsonSchema.tasks.instanceLimit', 'The number of instances of the task that are allowed to run simultaneously.'), default: 1 }, + instancePolicy: { + type: 'string', + enum: ['terminateNewest', 'terminateOldest', 'prompt', 'warn', 'silent'], + enumDescriptions: [ + nls.localize('JsonSchema.tasks.instancePolicy.terminateNewest', 'Terminates the newest instance.'), + nls.localize('JsonSchema.tasks.instancePolicy.terminateOldest', 'Terminates the oldest instance.'), + nls.localize('JsonSchema.tasks.instancePolicy.prompt', 'Asks which instance to terminate.'), + nls.localize('JsonSchema.tasks.instancePolicy.warn', 'Does nothing but warns that the instance limit has been reached.'), + nls.localize('JsonSchema.tasks.instancePolicy.silent', 'Does nothing.'), + ], + description: nls.localize('JsonSchema.tasks.instancePolicy', 'Policy to apply when instance limit is reached.'), + default: 'prompt' + } }, description: nls.localize('JsonSchema.tasks.runOptions', 'The task\'s run related options') }; diff --git a/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts b/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts index 25ebf8130b4..78ce57ca025 100644 --- a/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts +++ b/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts @@ -142,6 +142,7 @@ export interface IRunOptionsConfig { reevaluateOnRerun?: boolean; runOn?: string; instanceLimit?: number; + instancePolicy?: Tasks.InstancePolicy; } export interface ITaskIdentifier { @@ -708,12 +709,13 @@ export namespace RunOnOptions { } export namespace RunOptions { - const properties: IMetaData[] = [{ property: 'reevaluateOnRerun' }, { property: 'runOn' }, { property: 'instanceLimit' }]; + const properties: IMetaData[] = [{ property: 'reevaluateOnRerun' }, { property: 'runOn' }, { property: 'instanceLimit' }, { property: 'instancePolicy' }]; export function fromConfiguration(value: IRunOptionsConfig | undefined): Tasks.IRunOptions { return { reevaluateOnRerun: value ? value.reevaluateOnRerun : true, runOn: value ? RunOnOptions.fromString(value.runOn) : Tasks.RunOnOptions.default, - instanceLimit: value ? value.instanceLimit : 1 + instanceLimit: value ? value.instanceLimit : 1, + instancePolicy: value ? InstancePolicy.fromString(value.instancePolicy) : Tasks.InstancePolicy.prompt }; } @@ -726,6 +728,27 @@ export namespace RunOptions { } } +export namespace InstancePolicy { + export function fromString(value: string | undefined): Tasks.InstancePolicy { + if (!value) { + return Tasks.InstancePolicy.prompt; + } + switch (value.toLowerCase()) { + case 'terminatenewest': + return Tasks.InstancePolicy.terminateNewest; + case 'terminateoldest': + return Tasks.InstancePolicy.terminateOldest; + case 'warn': + return Tasks.InstancePolicy.warn; + case 'silent': + return Tasks.InstancePolicy.silent; + case 'prompt': + default: + return Tasks.InstancePolicy.prompt; + } + } +} + export interface IParseContext { workspaceFolder: IWorkspaceFolder; workspace: IWorkspace | undefined; diff --git a/src/vs/workbench/contrib/tasks/common/taskSystem.ts b/src/vs/workbench/contrib/tasks/common/taskSystem.ts index 565d8cbad25..b14eea3cf08 100644 --- a/src/vs/workbench/contrib/tasks/common/taskSystem.ts +++ b/src/vs/workbench/contrib/tasks/common/taskSystem.ts @@ -117,4 +117,5 @@ export interface ITaskSystem { customExecutionComplete(task: Task, result: number): Promise; isTaskVisible(task: Task): boolean; getTaskForTerminal(instanceId: number): Task | undefined; + getFirstInstance(task: Task): Task | undefined; } diff --git a/src/vs/workbench/contrib/tasks/common/tasks.ts b/src/vs/workbench/contrib/tasks/common/tasks.ts index 2c56cfc6a18..5e15a9fd76f 100644 --- a/src/vs/workbench/contrib/tasks/common/tasks.ts +++ b/src/vs/workbench/contrib/tasks/common/tasks.ts @@ -565,14 +565,23 @@ export enum RunOnOptions { folderOpen = 2 } +export const enum InstancePolicy { + terminateNewest = 'terminateNewest', + terminateOldest = 'terminateOldest', + prompt = 'prompt', + warn = 'warn', + silent = 'silent' +} + export interface IRunOptions { reevaluateOnRerun?: boolean; runOn?: RunOnOptions; instanceLimit?: number; + instancePolicy?: InstancePolicy; } export namespace RunOptions { - export const defaults: IRunOptions = { reevaluateOnRerun: true, runOn: RunOnOptions.default, instanceLimit: 1 }; + export const defaults: IRunOptions = { reevaluateOnRerun: true, runOn: RunOnOptions.default, instanceLimit: 1, instancePolicy: InstancePolicy.prompt }; } export abstract class CommonTask { diff --git a/src/vs/workbench/contrib/tasks/electron-sandbox/taskService.ts b/src/vs/workbench/contrib/tasks/electron-sandbox/taskService.ts index 6a5728c05e8..71b4a51157d 100644 --- a/src/vs/workbench/contrib/tasks/electron-sandbox/taskService.ts +++ b/src/vs/workbench/contrib/tasks/electron-sandbox/taskService.ts @@ -48,6 +48,7 @@ import { IThemeService } from '../../../../platform/theme/common/themeService.js import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js'; import { IAccessibilitySignalService } from '../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js'; +import { IChatService } from '../../chat/common/chatService.js'; interface IWorkspaceFolderConfigurationResult { workspaceFolder: IWorkspaceFolder; @@ -92,7 +93,8 @@ export class TaskService extends AbstractTaskService { @IThemeService themeService: IThemeService, @IInstantiationService instantiationService: IInstantiationService, @IRemoteAgentService remoteAgentService: IRemoteAgentService, - @IAccessibilitySignalService accessibilitySignalService: IAccessibilitySignalService + @IAccessibilitySignalService accessibilitySignalService: IAccessibilitySignalService, + @IChatService _chatService: IChatService ) { super(configurationService, markerService, @@ -129,7 +131,8 @@ export class TaskService extends AbstractTaskService { themeService, lifecycleService, remoteAgentService, - instantiationService + instantiationService, + _chatService ); this._register(lifecycleService.onBeforeShutdown(event => event.veto(this.beforeShutdown(), 'veto.tasks'))); } diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh index 7f6f5bdab7c..ee081dd6262 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh @@ -66,8 +66,8 @@ fi if [ -n "${VSCODE_ENV_REPLACE:-}" ]; then IFS=':' read -ra ADDR <<< "$VSCODE_ENV_REPLACE" for ITEM in "${ADDR[@]}"; do - VARNAME="${ITEM%%=*}" - VALUE="${ITEM#*=}" + VARNAME="$(echo $ITEM | cut -d "=" -f 1)" + VALUE="$(echo -e "$ITEM" | cut -d "=" -f 2-)" export $VARNAME="$VALUE" done builtin unset VSCODE_ENV_REPLACE @@ -75,8 +75,8 @@ fi if [ -n "${VSCODE_ENV_PREPEND:-}" ]; then IFS=':' read -ra ADDR <<< "$VSCODE_ENV_PREPEND" for ITEM in "${ADDR[@]}"; do - VARNAME="${ITEM%%=*}" - VALUE="${ITEM#*=}" + VARNAME="$(echo $ITEM | cut -d "=" -f 1)" + VALUE="$(echo -e "$ITEM" | cut -d "=" -f 2-)" export $VARNAME="$VALUE${!VARNAME}" done builtin unset VSCODE_ENV_PREPEND @@ -84,8 +84,8 @@ fi if [ -n "${VSCODE_ENV_APPEND:-}" ]; then IFS=':' read -ra ADDR <<< "$VSCODE_ENV_APPEND" for ITEM in "${ADDR[@]}"; do - VARNAME="${ITEM%%=*}" - VALUE="${ITEM#*=}" + VARNAME="$(echo $ITEM | cut -d "=" -f 1)" + VALUE="$(echo -e "$ITEM" | cut -d "=" -f 2-)" export $VARNAME="${!VARNAME}$VALUE" done builtin unset VSCODE_ENV_APPEND diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 index c29140f2ea9..4c3dbf35a94 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 @@ -139,7 +139,7 @@ if ($env:STARSHIP_SESSION_KEY) { elseif ($env:POSH_SESSION_ID) { [Console]::Write("$([char]0x1b)]633;P;PromptType=oh-my-posh`a") } -elseif ($Global:GitPromptSettings) { +elseif ((Test-Path variable:global:GitPromptSettings) -and $Global:GitPromptSettings) { [Console]::Write("$([char]0x1b)]633;P;PromptType=posh-git`a") } diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 3a8cde6a962..acad1225d79 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -331,12 +331,7 @@ export class TerminalChatWidget extends Disposable { const model = this._model.value; if (model) { this._inlineChatWidget.setChatModel(model, this._loadViewState()); - model.waitForInitialization().then(() => { - if (token.isCancellationRequested) { - return; - } - this._resetPlaceholder(); - }); + this._resetPlaceholder(); } if (!this._model.value) { throw new Error('Failed to start chat session'); diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/pwshCompletionProviderAddon.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/pwshCompletionProviderAddon.ts index 1b3429c304c..468d0d00114 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/pwshCompletionProviderAddon.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/pwshCompletionProviderAddon.ts @@ -45,10 +45,12 @@ const enum RequestCompletionsSequence { } export class PwshCompletionProviderAddon extends Disposable implements ITerminalAddon, ITerminalCompletionProvider { + + static readonly ID = 'pwsh-shell-integration'; + id: string = PwshCompletionProviderAddon.ID; triggerCharacters?: string[] | undefined; isBuiltin?: boolean = true; - static readonly ID = 'pwsh-shell-integration'; readonly shellTypes = [GeneralShellType.PowerShell]; private _lastUserDataTimestamp: number = 0; private _terminal?: Terminal; diff --git a/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts b/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts index 160800155fc..b3d885ffe09 100644 --- a/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts +++ b/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts @@ -710,6 +710,7 @@ export class CancelTestRunAction extends Action2 { id: TestCommandId.CancelTestRunAction, title: localize2('testing.cancelRun', 'Cancel Test Run'), icon: icons.testingCancelIcon, + category, keybinding: { weight: KeybindingWeight.WorkbenchContrib, primary: KeyChord(KeyMod.CtrlCmd | KeyCode.Semicolon, KeyMod.CtrlCmd | KeyCode.KeyX), @@ -722,6 +723,9 @@ export class CancelTestRunAction extends Action2 { ContextKeyExpr.equals('view', Testing.ExplorerViewId), ContextKeyExpr.equals(TestingContextKeys.isRunning.serialize(), true), ) + }, { + id: MenuId.CommandPalette, + when: TestingContextKeys.isRunning, }] }); } diff --git a/src/vs/workbench/contrib/testing/browser/testingDecorations.ts b/src/vs/workbench/contrib/testing/browser/testingDecorations.ts index dfe9127e8e4..ac47408247d 100644 --- a/src/vs/workbench/contrib/testing/browser/testingDecorations.ts +++ b/src/vs/workbench/contrib/testing/browser/testingDecorations.ts @@ -47,7 +47,7 @@ import { themeColorFromId } from '../../../../platform/theme/common/themeService import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; import { EditorLineNumberContextMenu, GutterActionsRegistry } from '../../codeEditor/browser/editorLineNumberMenu.js'; import { DefaultGutterClickAction, TestingConfigKeys, getTestingConfiguration } from '../common/configuration.js'; -import { Testing, labelForTestInState } from '../common/constants.js'; +import { TestCommandId, Testing, labelForTestInState } from '../common/constants.js'; import { TestId } from '../common/testId.js'; import { ITestProfileService } from '../common/testProfileService.js'; import { ITestResult, LiveTestResult, TestResultItemChangeReason } from '../common/testResult.js'; @@ -1063,6 +1063,11 @@ abstract class RunTestDecoration { () => this.commandService.executeCommand('vscode.peekTestError', test.item.extId))); } + if (resultItem?.computedState === TestResultState.Running) { + testActions.push(new Action('testing.gutter.cancel', localize('testing.cancelRun', 'Cancel Test Run'), undefined, undefined, + () => this.commandService.executeCommand(TestCommandId.CancelTestRunAction))); + } + testActions.push(new Action('testing.gutter.reveal', localize('reveal test', 'Reveal in Test Explorer'), undefined, undefined, () => this.commandService.executeCommand('_revealTestInExplorer', test.item.extId))); diff --git a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts index 6083f5719c6..67d5a0831aa 100644 --- a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts +++ b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts @@ -212,7 +212,7 @@ export class TestingExplorerView extends ViewPane { !alreadyIncluded // And it can be run using the current profile (if any) && isRunnableWithProfileOrBitset(element.test) - // And either it's a leaf node or most children are included, the include it. + // And either it's a leaf node or most children are included, then include it. && (visibleRunnableChildren === 0 || visibleRunnableChildren * 2 >= inTree.children.length) // And not if we're only showing a single of its children, since it // probably fans out later. (Worse case we'll directly include its single child) @@ -261,22 +261,8 @@ export class TestingExplorerView extends ViewPane { continue; } - // single controllers won't have visible root ID nodes, handle that case specially - if (!this.viewModel.tree.hasElement(element)) { - const visibleChildren = [...element.children].reduce((acc, c) => - this.viewModel.tree.hasElement(c) && this.viewModel.tree.getNode(c).visible ? acc + 1 : acc, 0); - - // note we intentionally check children > 0 here, unlike above, since - // we don't want to bother dispatching to controllers who have no discovered tests - if (element.children.size > 0 && visibleChildren * 2 >= element.children.size) { - include.add(element.test); - element.children.forEach(c => attempt(c, true)); - } else { - element.children.forEach(c => attempt(c, false)); - } - } else { - attempt(element, false); - } + include.add(element.test); + element.children.forEach(c => attempt(c, true)); } return { include: [...include], exclude }; diff --git a/src/vs/workbench/contrib/url/common/urlGlob.ts b/src/vs/workbench/contrib/url/common/urlGlob.ts index cc44a3dafc4..ff0456dcf03 100644 --- a/src/vs/workbench/contrib/url/common/urlGlob.ts +++ b/src/vs/workbench/contrib/url/common/urlGlob.ts @@ -5,89 +5,154 @@ import { URI } from '../../../../base/common/uri.js'; -// TODO: rewrite this to use URIs directly and validate each part individually -// instead of relying on memoization of the stringified URI. -export const testUrlMatchesGlob = (uri: URI, globUrl: string): boolean => { - let url = uri.with({ query: null, fragment: null }).toString(true); - const normalize = (url: string) => url.replace(/\/+$/, ''); - globUrl = normalize(globUrl); - url = normalize(url); +/** + * Normalizes a URL by removing trailing slashes and query/fragment components. + * @param url The URL to normalize. + * @returns URI - The normalized URI object. + */ +function normalizeURL(url: string | URI): URI { + const uri = typeof url === 'string' ? URI.parse(url) : url; + return uri.with({ + // Remove trailing slashes + path: uri.path.replace(/\/+$/, ''), + // Remove query and fragment + query: null, + fragment: null, + }); +} - const memo = Array.from({ length: url.length + 1 }).map(() => - Array.from({ length: globUrl.length + 1 }).map(() => undefined), +/** + * Checks if a given URL matches a glob URL pattern. + * The glob URL pattern can contain wildcards (*) and subdomain matching (*.) + * @param uri The URL to check. + * @param globUrl The glob URL pattern to match against. + * @returns boolean - True if the URL matches the glob URL pattern, false otherwise. + */ +export function testUrlMatchesGlob(uri: string | URI, globUrl: string): boolean { + const normalizedUrl = normalizeURL(uri); + let normalizedGlobUrl = normalizeURL(globUrl); + + const globHasScheme = /^[^./:]*:\/\//.test(globUrl); + // if the glob does not have a scheme we assume the scheme is http or https + // so if the url doesn't have a scheme of http or https we return false + if (!globHasScheme) { + if (normalizedUrl.scheme !== 'http' && normalizedUrl.scheme !== 'https') { + return false; + } + normalizedGlobUrl = normalizeURL(`${normalizedUrl.scheme}://${globUrl}`); + } + + return ( + doMemoUrlMatch(normalizedUrl.scheme, normalizedGlobUrl.scheme) && + // The authority is the only thing that should do port logic. + doMemoUrlMatch(normalizedUrl.authority, normalizedGlobUrl.authority, true) && + ( + // + normalizedGlobUrl.path === '/' || + doMemoUrlMatch(normalizedUrl.path, normalizedGlobUrl.path) + ) + ); +} + +/** + * @param normalizedUrlPart The normalized URL part to match. + * @param normalizedGlobUrlPart The normalized glob URL part to match against. + * @param includePortLogic Whether to include port logic in the matching process. + * @returns boolean - True if the URL part matches the glob URL part, false otherwise. + */ +function doMemoUrlMatch( + normalizedUrlPart: string, + normalizedGlobUrlPart: string, + includePortLogic: boolean = false, +) { + const memo = Array.from({ length: normalizedUrlPart.length + 1 }).map(() => + Array.from({ length: normalizedGlobUrlPart.length + 1 }).map(() => undefined), ); - if (/^[^./:]*:\/\//.test(globUrl)) { - return doUrlMatch(memo, url, globUrl, 0, 0); - } + return doUrlPartMatch(memo, includePortLogic, normalizedUrlPart, normalizedGlobUrlPart, 0, 0); +} - const scheme = /^(https?):\/\//.exec(url)?.[1]; - if (scheme) { - return doUrlMatch(memo, url, `${scheme}://${globUrl}`, 0, 0); - } - - return false; -}; - -const doUrlMatch = ( +/** + * Recursively checks if a URL part matches a glob URL part. + * This function uses memoization to avoid recomputing results for the same inputs. + * It handles various cases such as exact matches, wildcard matches, and port logic. + * @param memo A memoization table to avoid recomputing results for the same inputs. + * @param includePortLogic Whether to include port logic in the matching process. + * @param urlPart The URL part to match with. + * @param globUrlPart The glob URL part to match against. + * @param urlOffset The current offset in the URL part. + * @param globUrlOffset The current offset in the glob URL part. + * @returns boolean - True if the URL part matches the glob URL part, false otherwise. + */ +function doUrlPartMatch( memo: (boolean | undefined)[][], - url: string, - globUrl: string, + includePortLogic: boolean, + urlPart: string, + globUrlPart: string, urlOffset: number, - globUrlOffset: number, -): boolean => { + globUrlOffset: number +): boolean { if (memo[urlOffset]?.[globUrlOffset] !== undefined) { return memo[urlOffset][globUrlOffset]!; } const options = []; - // Endgame. - // Fully exact match - if (urlOffset === url.length) { - return globUrlOffset === globUrl.length; + // We've reached the end of the url. + if (urlOffset === urlPart.length) { + // We're also at the end of the glob url as well so we have an exact match. + if (globUrlOffset === globUrlPart.length) { + return true; + } + + if (includePortLogic && globUrlPart[globUrlOffset] + globUrlPart[globUrlOffset + 1] === ':*') { + // any port match. Consume a port if it exists otherwise nothing. Always consume the base. + return globUrlOffset + 2 === globUrlPart.length; + } + + return false; } // Some path remaining in url - if (globUrlOffset === globUrl.length) { - const remaining = url.slice(urlOffset); + if (globUrlOffset === globUrlPart.length) { + const remaining = urlPart.slice(urlOffset); return remaining[0] === '/'; } - if (url[urlOffset] === globUrl[globUrlOffset]) { + if (urlPart[urlOffset] === globUrlPart[globUrlOffset]) { // Exact match. - options.push(doUrlMatch(memo, url, globUrl, urlOffset + 1, globUrlOffset + 1)); + options.push(doUrlPartMatch(memo, includePortLogic, urlPart, globUrlPart, urlOffset + 1, globUrlOffset + 1)); } - if (globUrl[globUrlOffset] + globUrl[globUrlOffset + 1] === '*.') { + if (globUrlPart[globUrlOffset] + globUrlPart[globUrlOffset + 1] === '*.') { // Any subdomain match. Either consume one thing that's not a / or : and don't advance base or consume nothing and do. - if (!['/', ':'].includes(url[urlOffset])) { - options.push(doUrlMatch(memo, url, globUrl, urlOffset + 1, globUrlOffset)); + if (!['/', ':'].includes(urlPart[urlOffset])) { + options.push(doUrlPartMatch(memo, includePortLogic, urlPart, globUrlPart, urlOffset + 1, globUrlOffset)); } - options.push(doUrlMatch(memo, url, globUrl, urlOffset, globUrlOffset + 2)); + options.push(doUrlPartMatch(memo, includePortLogic, urlPart, globUrlPart, urlOffset, globUrlOffset + 2)); } - if (globUrl[globUrlOffset] === '*') { + if (globUrlPart[globUrlOffset] === '*') { // Any match. Either consume one thing and don't advance base or consume nothing and do. - if (urlOffset + 1 === url.length) { + if (urlOffset + 1 === urlPart.length) { // If we're at the end of the input url consume one from both. - options.push(doUrlMatch(memo, url, globUrl, urlOffset + 1, globUrlOffset + 1)); + options.push(doUrlPartMatch(memo, includePortLogic, urlPart, globUrlPart, urlOffset + 1, globUrlOffset + 1)); } else { - options.push(doUrlMatch(memo, url, globUrl, urlOffset + 1, globUrlOffset)); + options.push(doUrlPartMatch(memo, includePortLogic, urlPart, globUrlPart, urlOffset + 1, globUrlOffset)); } - options.push(doUrlMatch(memo, url, globUrl, urlOffset, globUrlOffset + 1)); + options.push(doUrlPartMatch(memo, includePortLogic, urlPart, globUrlPart, urlOffset, globUrlOffset + 1)); } - if (globUrl[globUrlOffset] + globUrl[globUrlOffset + 1] === ':*') { - // any port match. Consume a port if it exists otherwise nothing. Always comsume the base. - if (url[urlOffset] === ':') { + if (includePortLogic && globUrlPart[globUrlOffset] + globUrlPart[globUrlOffset + 1] === ':*') { + // any port match. Consume a port if it exists otherwise nothing. Always consume the base. + if (urlPart[urlOffset] === ':') { let endPortIndex = urlOffset + 1; - do { endPortIndex++; } while (/[0-9]/.test(url[endPortIndex])); - options.push(doUrlMatch(memo, url, globUrl, endPortIndex, globUrlOffset + 2)); + do { endPortIndex++; } while (/[0-9]/.test(urlPart[endPortIndex])); + options.push(doUrlPartMatch(memo, includePortLogic, urlPart, globUrlPart, endPortIndex, globUrlOffset + 2)); } else { - options.push(doUrlMatch(memo, url, globUrl, urlOffset, globUrlOffset + 2)); + options.push(doUrlPartMatch(memo, includePortLogic, urlPart, globUrlPart, urlOffset, globUrlOffset + 2)); } } return (memo[urlOffset][globUrlOffset] = options.some(a => a === true)); -}; +} diff --git a/src/vs/workbench/contrib/url/test/browser/trustedDomains.test.ts b/src/vs/workbench/contrib/url/test/browser/trustedDomains.test.ts index 50b2af6a09d..390d9897097 100644 --- a/src/vs/workbench/contrib/url/test/browser/trustedDomains.test.ts +++ b/src/vs/workbench/contrib/url/test/browser/trustedDomains.test.ts @@ -113,4 +113,9 @@ suite('Link protection domain matching', () => { linkAllowedByRules('https://github.com/login/oauth/authorize?foo=4', ['https://github.com/login/oauth/authorize']); linkAllowedByRules('https://github.com/login/oauth/authorize#foo', ['https://github.com/login/oauth/authorize']); }); + + test('ensure individual parts of url are compared and wildcard does not leak out', () => { + linkNotAllowedByRules('https://x.org/github.com', ['https://*.github.com']); + linkNotAllowedByRules('https://x.org/y.github.com', ['https://*.github.com']); + }); }); diff --git a/src/vs/workbench/contrib/webview/browser/pre/service-worker.ts b/src/vs/workbench/contrib/webview/browser/pre/service-worker.ts index ab6f674be34..cfa78394df8 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/service-worker.ts +++ b/src/vs/workbench/contrib/webview/browser/pre/service-worker.ts @@ -308,7 +308,7 @@ async function processResourceRequest( headers['Cross-Origin-Opener-Policy'] = 'same-origin'; } - const response = new Response(entry.data, { + const response = new Response(entry.data as Uint8Array, { status: 200, headers }); diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts index e33143a6112..8d728275a51 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts @@ -21,7 +21,7 @@ import { DisposableStore, toDisposable } from '../../../../base/common/lifecycle import { ILink, LinkedText, parseLinkedText } from '../../../../base/common/linkedText.js'; import { parse } from '../../../../base/common/marshalling.js'; import { Schemas, matchesScheme } from '../../../../base/common/network.js'; -import { isMacintosh, OS } from '../../../../base/common/platform.js'; +import { OS } from '../../../../base/common/platform.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { assertIsDefined } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; @@ -412,7 +412,7 @@ export class GettingStartedPage extends EditorPane { if (this.contextService.contextMatchesRules(ContextKeyExpr.and(WorkbenchStateContext.isEqualTo('workspace')))) { this.commandService.executeCommand(OpenFolderViaWorkspaceAction.ID); } else { - this.commandService.executeCommand(isMacintosh ? 'workbench.action.files.openFileFolder' : 'workbench.action.files.openFolder'); + this.commandService.executeCommand('workbench.action.files.openFolder'); } break; } diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts b/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts index 59b7d5e9478..8f6da7a6a90 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts @@ -251,9 +251,9 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ content: { type: 'steps', steps: [ - createCopilotSetupStep('CopilotSetupSignedOut', CopilotSignedOutButton, 'chatSetupSignedOut', true), - createCopilotSetupStep('CopilotSetupComplete', CopilotCompleteButton, 'chatSetupInstalled && (chatPlanPro || chatPlanProPlus || chatPlanBusiness || chatPlanEnterprise || chatPlanLimited)', false), - createCopilotSetupStep('CopilotSetupSignedIn', CopilotSignedInButton, '!chatSetupSignedOut && (!chatSetupInstalled || chatPlanCanSignUp)', true), + createCopilotSetupStep('CopilotSetupSignedOut', CopilotSignedOutButton, 'chatEntitlementSignedOut', true), + createCopilotSetupStep('CopilotSetupComplete', CopilotCompleteButton, 'chatSetupInstalled && !chatSetupDisabled && (chatPlanPro || chatPlanProPlus || chatPlanBusiness || chatPlanEnterprise || chatPlanLimited)', false), + createCopilotSetupStep('CopilotSetupSignedIn', CopilotSignedInButton, '!chatEntitlementSignedOut && (!chatSetupInstalled || chatSetupDisabled || chatPlanCanSignUp)', true), { id: 'pickColorTheme', title: localize('gettingStarted.pickColor.title', "Choose your theme"), diff --git a/src/vs/workbench/electron-sandbox/actions/windowActions.ts b/src/vs/workbench/electron-sandbox/actions/windowActions.ts index 424db028d1c..f76d6c79c03 100644 --- a/src/vs/workbench/electron-sandbox/actions/windowActions.ts +++ b/src/vs/workbench/electron-sandbox/actions/windowActions.ts @@ -484,8 +484,7 @@ export class DisableWindowAlwaysOnTopAction extends Action2 { super({ id: DisableWindowAlwaysOnTopAction.ID, title: localize('disableWindowAlwaysOnTop', "Unset Always on Top"), - icon: Codicon.pin, - toggled: { condition: IsWindowAlwaysOnTopContext, icon: Codicon.pinned }, + icon: Codicon.pinned, menu: { id: MenuId.LayoutControlMenu, when: ContextKeyExpr.and(IsWindowAlwaysOnTopContext, IsAuxiliaryTitleBarContext), diff --git a/src/vs/workbench/electron-sandbox/desktop.contribution.ts b/src/vs/workbench/electron-sandbox/desktop.contribution.ts index e4b863b0b4a..1b38c2e8019 100644 --- a/src/vs/workbench/electron-sandbox/desktop.contribution.ts +++ b/src/vs/workbench/electron-sandbox/desktop.contribution.ts @@ -267,7 +267,7 @@ import { registerWorkbenchContribution2, WorkbenchPhase } from '../common/contri 'enum': ['native', 'custom'], 'default': 'native', 'scope': ConfigurationScope.APPLICATION, - 'description': localize('dialogStyle', "Adjust the appearance of dialog windows.") + 'description': localize('dialogStyle', "Adjust the appearance of dialogs to be native by the OS or custom.") }, 'window.nativeTabs': { 'type': 'boolean', @@ -426,6 +426,12 @@ import { registerWorkbenchContribution2, WorkbenchPhase } from '../common/contri description: localize('argv.passwordStore', "Configures the backend used to store secrets on Linux. This argument is ignored on Windows & macOS.") }; } + if (isWindows) { + schema.properties!['enable-rdp-display-tracking'] = { + type: 'boolean', + description: localize('argv.enableRDPDisplayTracking', "Ensures that maximized windows gets restored to correct display during RDP reconnection.") + }; + } jsonRegistry.registerSchema(argvDefinitionFileSchemaId, schema); })(); diff --git a/src/vs/workbench/services/accounts/common/defaultAccount.ts b/src/vs/workbench/services/accounts/common/defaultAccount.ts index 79c94410484..7cfd7775ade 100644 --- a/src/vs/workbench/services/accounts/common/defaultAccount.ts +++ b/src/vs/workbench/services/accounts/common/defaultAccount.ts @@ -201,7 +201,7 @@ export class DefaultAccountManagementContribution extends Disposable implements } private async getDefaultAccountFromAuthenticatedSessions(authProviderId: string, enterpriseAuthProviderId: string, enterpriseAuthProviderConfig: string, scopes: string[], tokenEntitlementUrl: string, chatEntitlementUrl: string): Promise { - const id = this.configurationService.getValue(enterpriseAuthProviderConfig) ? enterpriseAuthProviderId : authProviderId; + const id = this.configurationService.getValue(enterpriseAuthProviderConfig) === enterpriseAuthProviderId ? enterpriseAuthProviderId : authProviderId; const sessions = await this.authenticationService.getSessions(id, undefined, undefined, true); const session = sessions.find(s => this.scopesMatch(s.scopes, scopes)); @@ -297,7 +297,7 @@ export class DefaultAccountManagementContribution extends Disposable implements }); } run(): Promise { - const id = that.configurationService.getValue(enterpriseAuthProviderConfig) ? enterpriseAuthProviderId : authProviderId; + const id = that.configurationService.getValue(enterpriseAuthProviderConfig) === enterpriseAuthProviderId ? enterpriseAuthProviderId : authProviderId; return that.authenticationService.createSession(id, scopes); } })); diff --git a/src/vs/workbench/services/authentication/browser/authenticationService.ts b/src/vs/workbench/services/authentication/browser/authenticationService.ts index 34bae5b5101..16c03601fae 100644 --- a/src/vs/workbench/services/authentication/browser/authenticationService.ts +++ b/src/vs/workbench/services/authentication/browser/authenticationService.ts @@ -18,6 +18,8 @@ import { ActivationKind, IExtensionService } from '../../extensions/common/exten import { ILogService } from '../../../../platform/log/common/log.js'; import { IJSONSchema } from '../../../../base/common/jsonSchema.js'; import { ExtensionsRegistry } from '../../extensions/common/extensionsRegistry.js'; +import { match } from '../../../../base/common/glob.js'; +import { URI } from '../../../../base/common/uri.js'; export function getAuthenticationProviderActivationEvent(id: string): string { return `onAuthenticationRequest:${id}`; } @@ -250,10 +252,17 @@ export class AuthenticationService extends Disposable implements IAuthentication return accounts; } - async getSessions(id: string, scopes?: string[], account?: AuthenticationSessionAccount, activateImmediate: boolean = false): Promise> { + async getSessions(id: string, scopes?: string[], account?: AuthenticationSessionAccount, activateImmediate: boolean = false, issuer?: URI): Promise> { const authProvider = this._authenticationProviders.get(id) || await this.tryActivateProvider(id, activateImmediate); if (authProvider) { - return await authProvider.getSessions(scopes, { account }); + // Check if the issuer is in the list of supported issuers + if (issuer) { + const issuerStr = issuer.toString(true); + if (!authProvider.issuers?.some(i => match(i.toString(true), issuerStr))) { + throw new Error(`The issuer '${issuerStr}' is not supported by the authentication provider '${id}'.`); + } + } + return await authProvider.getSessions(scopes, { account, issuer }); } else { throw new Error(`No authentication provider '${id}' is currently registered.`); } @@ -263,7 +272,8 @@ export class AuthenticationService extends Disposable implements IAuthentication const authProvider = this._authenticationProviders.get(id) || await this.tryActivateProvider(id, !!options?.activateImmediate); if (authProvider) { return await authProvider.createSession(scopes, { - account: options?.account + account: options?.account, + issuer: options?.issuer }); } else { throw new Error(`No authentication provider '${id}' is currently registered.`); @@ -279,6 +289,22 @@ export class AuthenticationService extends Disposable implements IAuthentication } } + // Not used yet but will be... + async getOrActivateProviderIdForIssuer(issuer: URI): Promise { + const issuerStr = issuer.toString(true); + const providers = this._declaredProviders + .filter(p => !!p.issuerGlobs?.some(i => match(i, issuerStr))); + // TODO:@TylerLeonhardt fan out? + for (const provider of providers) { + const activeProvider = await this.tryActivateProvider(provider.id, true); + // Check the resolved issuers + if (activeProvider.issuers?.some(i => match(i.toString(true), issuerStr))) { + return activeProvider.id; + } + } + return undefined; + } + private async tryActivateProvider(providerId: string, activateImmediate: boolean): Promise { await this._extensionService.activateByEvent(getAuthenticationProviderActivationEvent(providerId), activateImmediate ? ActivationKind.Immediate : ActivationKind.Normal); let provider = this._authenticationProviders.get(providerId); diff --git a/src/vs/workbench/services/authentication/common/authentication.ts b/src/vs/workbench/services/authentication/common/authentication.ts index f3643cf2365..bf2d0d588d0 100644 --- a/src/vs/workbench/services/authentication/common/authentication.ts +++ b/src/vs/workbench/services/authentication/common/authentication.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { Event } from '../../../../base/common/event.js'; +import { URI } from '../../../../base/common/uri.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; /** @@ -32,6 +33,7 @@ export interface AuthenticationSessionsChangeEvent { export interface AuthenticationProviderInformation { id: string; label: string; + issuerGlobs?: ReadonlyArray; } export interface IAuthenticationCreateSessionOptions { @@ -41,6 +43,11 @@ export interface IAuthenticationCreateSessionOptions { * attempt to return the sessions that are only related to this account. */ account?: AuthenticationSessionAccount; + /** + * The issuer URI to use for this creation request. If passed in, first we validate that + * the provider can use this issuer, then it is passed down to the auth provider. + */ + issuer?: URI; } export interface AllowedExtension { @@ -138,11 +145,14 @@ export interface IAuthenticationService { /** * Gets all sessions that satisfy the given scopes from the provider with the given id + * TODO:@TylerLeonhardt Refactor this to use an options bag for account and issuer * @param id The id of the provider to ask for a session * @param scopes The scopes for the session + * @param account The account for the session * @param activateImmediate If true, the provider should activate immediately if it is not already + * @param issuer The issuer for the session */ - getSessions(id: string, scopes?: string[], account?: AuthenticationSessionAccount, activateImmediate?: boolean): Promise>; + getSessions(id: string, scopes?: string[], account?: AuthenticationSessionAccount, activateImmediate?: boolean, issuer?: URI): Promise>; /** * Creates an AuthenticationSession with the given provider and scopes @@ -158,6 +168,12 @@ export interface IAuthenticationService { * @param sessionId The id of the session to remove */ removeSession(providerId: string, sessionId: string): Promise; + + /** + * Gets a provider id for a specified issuer + * @param issuer The issuer url that this provider is responsible for + */ + getOrActivateProviderIdForIssuer(issuer: URI): Promise; } // TODO: Move this into MainThreadAuthentication @@ -224,6 +240,11 @@ export interface IAuthenticationProviderSessionOptions { * attempt to return the sessions that are only related to this account. */ account?: AuthenticationSessionAccount; + /** + * The issuer that is being asked about. If this is passed in, the provider should + * attempt to return sessions that are only related to this issuer. + */ + issuer?: URI; } /** @@ -240,6 +261,11 @@ export interface IAuthenticationProvider { */ readonly label: string; + /** + * The resolved issuers. These can still contain globs, but should be concrete URIs + */ + readonly issuers?: ReadonlyArray; + /** * Indicates whether the authentication provider supports multiple accounts. */ diff --git a/src/vs/workbench/services/authentication/test/browser/authenticationService.test.ts b/src/vs/workbench/services/authentication/test/browser/authenticationService.test.ts index c4b40487937..4d7ee6f8e80 100644 --- a/src/vs/workbench/services/authentication/test/browser/authenticationService.test.ts +++ b/src/vs/workbench/services/authentication/test/browser/authenticationService.test.ts @@ -6,6 +6,7 @@ import assert from 'assert'; import { Emitter, Event } from '../../../../../base/common/event.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { URI } from '../../../../../base/common/uri.js'; import { AuthenticationAccessService } from '../../browser/authenticationAccessService.js'; import { AuthenticationService } from '../../browser/authenticationService.js'; import { AuthenticationProviderInformation, AuthenticationSessionsChangeEvent, IAuthenticationProvider } from '../../common/authentication.js'; @@ -127,10 +128,104 @@ suite('AuthenticationService', () => { // Assert that the retrieved provider is the same as the registered provider assert.deepEqual(retrievedProvider, provider); }); + + test('getOrActivateProviderIdForIssuer - should return undefined when no provider matches the issuer', async () => { + const issuer = URI.parse('https://example.com'); + const result = await authenticationService.getOrActivateProviderIdForIssuer(issuer); + assert.strictEqual(result, undefined); + }); + + test('getOrActivateProviderIdForIssuer - should return provider id if issuerGlobs matches and issuers match', async () => { + // Register a declared provider with an issuer glob + const provider: AuthenticationProviderInformation = { + id: 'github', + label: 'GitHub', + issuerGlobs: ['https://github.com/*'] + }; + authenticationService.registerDeclaredAuthenticationProvider(provider); + + // Register an authentication provider with matching issuers + const authProvider = createProvider({ + id: 'github', + label: 'GitHub', + issuers: [URI.parse('https://github.com/login')] + }); + authenticationService.registerAuthenticationProvider('github', authProvider); + + // Test with a matching URI + const issuer = URI.parse('https://github.com/login'); + const result = await authenticationService.getOrActivateProviderIdForIssuer(issuer); + + // Verify the result + assert.strictEqual(result, 'github'); + }); + + test('getOrActivateProviderIdForIssuer - should return undefined if issuerGlobs match but issuers do not match', async () => { + // Register a declared provider with an issuer glob + const provider: AuthenticationProviderInformation = { + id: 'github', + label: 'GitHub', + issuerGlobs: ['https://github.com/*'] + }; + authenticationService.registerDeclaredAuthenticationProvider(provider); + + // Register an authentication provider with non-matching issuers + const authProvider = createProvider({ + id: 'github', + label: 'GitHub', + issuers: [URI.parse('https://github.com/different')] + }); + authenticationService.registerAuthenticationProvider('github', authProvider); + + // Test with a non-matching URI + const issuer = URI.parse('https://github.com/login'); + const result = await authenticationService.getOrActivateProviderIdForIssuer(issuer); + + // Verify the result + assert.strictEqual(result, undefined); + }); + + test('getOrActivateProviderIdForIssuer - should check multiple providers and return the first match', async () => { + // Register two declared providers with issuer globs + const provider1: AuthenticationProviderInformation = { + id: 'github', + label: 'GitHub', + issuerGlobs: ['https://github.com/*'] + }; + const provider2: AuthenticationProviderInformation = { + id: 'microsoft', + label: 'Microsoft', + issuerGlobs: ['https://login.microsoftonline.com/*'] + }; + authenticationService.registerDeclaredAuthenticationProvider(provider1); + authenticationService.registerDeclaredAuthenticationProvider(provider2); + + // Register authentication providers + const githubProvider = createProvider({ + id: 'github', + label: 'GitHub', + issuers: [URI.parse('https://github.com/different')] + }); + authenticationService.registerAuthenticationProvider('github', githubProvider); + + const microsoftProvider = createProvider({ + id: 'microsoft', + label: 'Microsoft', + issuers: [URI.parse('https://login.microsoftonline.com/common')] + }); + authenticationService.registerAuthenticationProvider('microsoft', microsoftProvider); + + // Test with a URI that should match the second provider + const issuer = URI.parse('https://login.microsoftonline.com/common'); + const result = await authenticationService.getOrActivateProviderIdForIssuer(issuer); + + // Verify the result + assert.strictEqual(result, 'microsoft'); + }); }); suite('authenticationSessions', () => { - test('getSessions', async () => { + test('getSessions - base case', async () => { let isCalled = false; const provider = createProvider({ getSessions: async () => { @@ -145,6 +240,19 @@ suite('AuthenticationService', () => { assert.ok(isCalled); }); + test('getSessions - issuer is not registered', async () => { + let isCalled = false; + const provider = createProvider({ + getSessions: async () => { + isCalled = true; + return [createSession()]; + }, + }); + authenticationService.registerAuthenticationProvider(provider.id, provider); + assert.rejects(() => authenticationService.getSessions(provider.id, [], undefined, undefined, URI.parse('https://example.com'))); + assert.ok(!isCalled); + }); + test('createSession', async () => { const emitter = new Emitter(); const provider = createProvider({ diff --git a/src/vs/workbench/services/browserElements/browser/webBrowserElementsService.ts b/src/vs/workbench/services/browserElements/browser/webBrowserElementsService.ts new file mode 100644 index 00000000000..6c0b5005a7d --- /dev/null +++ b/src/vs/workbench/services/browserElements/browser/webBrowserElementsService.ts @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IElementData } from '../../../../platform/browserElements/common/browserElements.js'; +import { IRectangle } from '../../../../platform/window/common/window.js'; +import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js'; +import { IBrowserElementsService } from './browserElementsService.js'; + +class WebBrowserElementsService implements IBrowserElementsService { + _serviceBrand: undefined; + + constructor() { } + + async getElementData(rect: IRectangle, token: CancellationToken): Promise { + throw new Error('Not implemented'); + } +} + +registerSingleton(IBrowserElementsService, WebBrowserElementsService, InstantiationType.Delayed); diff --git a/src/vs/workbench/services/configuration/test/browser/configurationEditing.test.ts b/src/vs/workbench/services/configuration/test/browser/configurationEditing.test.ts index a1b78e0c8ff..f202e4fe541 100644 --- a/src/vs/workbench/services/configuration/test/browser/configurationEditing.test.ts +++ b/src/vs/workbench/services/configuration/test/browser/configurationEditing.test.ts @@ -193,7 +193,7 @@ suite('ConfigurationEditing', () => { test('do not notify error', async () => { instantiationService.stub(ITextFileService, 'isDirty', true); const target = sinon.stub(); - instantiationService.stub(INotificationService, { prompt: target, _serviceBrand: undefined, filter: false, onDidAddNotification: undefined!, onDidRemoveNotification: undefined!, onDidChangeFilter: undefined!, notify: null!, error: null!, info: null!, warn: null!, status: null!, setFilter: null!, getFilter: null!, getFilters: null!, removeFilter: null! }); + instantiationService.stub(INotificationService, { prompt: target, _serviceBrand: undefined, filter: false, onDidChangeFilter: undefined!, notify: null!, error: null!, info: null!, warn: null!, status: null!, setFilter: null!, getFilter: null!, getFilters: null!, removeFilter: null! }); try { await testObject.writeConfiguration(EditableConfigurationTarget.USER_LOCAL, { key: 'configurationEditing.service.testSetting', value: 'value' }, { donotNotifyError: true }); } catch (error) { diff --git a/src/vs/workbench/services/configurationResolver/common/configurationResolverExpression.ts b/src/vs/workbench/services/configurationResolver/common/configurationResolverExpression.ts index 1cabb263d78..25d505123e9 100644 --- a/src/vs/workbench/services/configurationResolver/common/configurationResolverExpression.ts +++ b/src/vs/workbench/services/configurationResolver/common/configurationResolverExpression.ts @@ -65,7 +65,7 @@ interface IReplacementLocation { export class ConfigurationResolverExpression implements IConfigurationResolverExpression { public static readonly VARIABLE_LHS = '${'; - private locations = new Map(); + private readonly locations = new Map(); private root: T; private stringRoot: boolean; /** @@ -173,17 +173,14 @@ export class ConfigurationResolverExpression implements IConfigurationResolve } for (const [key, value] of Object.entries(obj)) { + this.parseString(obj, key, key, true); // parse key + if (typeof value === 'string') { this.parseString(obj, key, value); } else { this.parseObject(value); } } - - // only after all values are marked for replacement, we can collect keys that have to be replaced - for (const [key] of Object.entries(obj)) { - this.parseString(obj, key, key, true); - } } private parseString(object: any, propertyName: string | number, value: string, replaceKeyName?: boolean, replacementPath?: string[]): void { @@ -281,15 +278,26 @@ export class ConfigurationResolverExpression implements IConfigurationResolve const newKey = propertyName.replaceAll(replacement.id, data.value); delete object[propertyName]; object[newKey] = value; + this._renameKeyInLocations(object, propertyName, newKey); this.parseString(object, newKey, data.value, true, path); } else { - this.parseString(object, propertyName, data.value, false, path); object[propertyName] = object[propertyName].replaceAll(replacement.id, data.value); + this.parseString(object, propertyName, data.value, false, path); } path.pop(); } + private _renameKeyInLocations(obj: object, oldKey: string, newKey: string) { + for (const location of this.locations.values()) { + for (const loc of location.locations) { + if (loc.object === obj && loc.propertyName === oldKey) { + loc.propertyName = newKey; + } + } + } + } + public toObject(): T { // If we wrapped a string, unwrap it if (this.stringRoot) { diff --git a/src/vs/workbench/services/configurationResolver/test/electron-sandbox/configurationResolverService.test.ts b/src/vs/workbench/services/configurationResolver/test/electron-sandbox/configurationResolverService.test.ts index 68d24b67cf9..e5afb8970b9 100644 --- a/src/vs/workbench/services/configurationResolver/test/electron-sandbox/configurationResolverService.test.ts +++ b/src/vs/workbench/services/configurationResolver/test/electron-sandbox/configurationResolverService.test.ts @@ -1010,4 +1010,53 @@ suite('ConfigurationResolverExpression', () => { 'key that is username: testuser': 'cool!' }); }); + + test('resolves nested values 2 (#245798)', () => { + const expr = ConfigurationResolverExpression.parse({ + env: { + SITE: "${input:site}", + TLD: "${input:tld}", + HOST: "${input:host}", + }, + }); + + for (const r of expr.unresolved()) { + if (r.arg === 'site') { + expr.resolve(r, 'example'); + } else if (r.arg === 'tld') { + expr.resolve(r, 'com'); + } else if (r.arg === 'host') { + expr.resolve(r, 'local.${input:site}.${input:tld}'); + } + } + + assert.deepStrictEqual(expr.toObject(), { + env: { + SITE: 'example', + TLD: 'com', + HOST: 'local.example.com' + } + }); + }); + + test('out-of-order key resolution (#248550)', () => { + const expr = ConfigurationResolverExpression.parse({ + '${input:key}': "${input:value}", + }); + + for (const r of expr.unresolved()) { + if (r.arg === 'key') { + expr.resolve(r, 'the-key'); + } + } + for (const r of expr.unresolved()) { + if (r.arg === 'value') { + expr.resolve(r, 'the-value'); + } + } + + assert.deepStrictEqual(expr.toObject(), { + 'the-key': 'the-value' + }); + }); }); diff --git a/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts b/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts index b280750431f..4c7a927bd43 100644 --- a/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts +++ b/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts @@ -159,7 +159,7 @@ export class SimpleFileDialog extends Disposable implements ISimpleFileDialog { this.getShowDotFiles(); const disposableStore = this._register(new DisposableStore()); - this.storageService.onDidChangeValue(StorageScope.WORKSPACE, 'remoteFileDialog.showDotFiles', disposableStore)(async _ => { + disposableStore.add(this.storageService.onDidChangeValue(StorageScope.WORKSPACE, 'remoteFileDialog.showDotFiles', disposableStore)(async _ => { this.getShowDotFiles(); this.setButtons(); const startingValue = this.filePickBox.value; @@ -167,7 +167,7 @@ export class SimpleFileDialog extends Disposable implements ISimpleFileDialog { this.filePickBox.value = folderValue; await this.tryUpdateItems(folderValue, this.currentFolder, true); this.filePickBox.value = startingValue; - }); + })); } private setShowDotFiles(showDotFiles: boolean) { diff --git a/src/vs/workbench/services/editor/common/editorGroupsService.ts b/src/vs/workbench/services/editor/common/editorGroupsService.ts index f1154ed8d47..d38e77b3bb9 100644 --- a/src/vs/workbench/services/editor/common/editorGroupsService.ts +++ b/src/vs/workbench/services/editor/common/editorGroupsService.ts @@ -229,15 +229,20 @@ export interface IEditorGroupsContainer { */ readonly onDidChangeGroupMaximized: Event; + /** + * An event that notifies when container options change. + */ + readonly onDidChangeEditorPartOptions: Event; + /** * A property that indicates when groups have been created - * and are ready to be used in the editor part. + * and are ready to be used in the container. */ readonly isReady: boolean; /** * A promise that resolves when groups have been created - * and are ready to be used in the editor part. + * and are ready to be used in the container. * * Await this promise to safely work on the editor groups model * (for example, install editor group listeners). @@ -249,7 +254,7 @@ export interface IEditorGroupsContainer { /** * A promise that resolves when groups have been restored in - * the editor part. + * the container. * * For groups with active editor, the promise will resolve * when the visible editor has finished to resolve. @@ -260,7 +265,7 @@ export interface IEditorGroupsContainer { readonly whenRestored: Promise; /** - * Find out if the editor part has UI state to restore + * Find out if the container has UI state to restore * from a previous session. */ readonly hasRestorableState: boolean; @@ -293,6 +298,16 @@ export interface IEditorGroupsContainer { */ readonly orientation: GroupOrientation; + /** + * Access the options of the container. + */ + readonly partOptions: IEditorPartOptions; + + /** + * Enforce container options temporarily. + */ + enforcePartOptions(options: DeepPartial): IDisposable; + /** * Get all groups that are currently visible in the container. * @@ -474,11 +489,6 @@ export interface IEditorPart extends IEditorGroupsContainer { * Find out if the editor layout is currently centered. */ isLayoutCentered(): boolean; - - /** - * Enforce editor part options temporarily. - */ - enforcePartOptions(options: DeepPartial): IDisposable; } export interface IAuxiliaryEditorPart extends IEditorPart { @@ -551,16 +561,6 @@ export interface IEditorGroupsService extends IEditorGroupsContainer { */ getPart(container: unknown /* HTMLElement */): IEditorPart; - /** - * Access the options of the editor part. - */ - readonly partOptions: IEditorPartOptions; - - /** - * An event that notifies when editor part options change. - */ - readonly onDidChangeEditorPartOptions: Event; - /** * Opens a new window with a full editor part instantiated * in there at the optional position and size on screen. diff --git a/src/vs/workbench/services/host/browser/host.ts b/src/vs/workbench/services/host/browser/host.ts index 964816da080..f83c4b79e84 100644 --- a/src/vs/workbench/services/host/browser/host.ts +++ b/src/vs/workbench/services/host/browser/host.ts @@ -6,6 +6,7 @@ import { VSBuffer } from '../../../../base/common/buffer.js'; import { Event } from '../../../../base/common/event.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; +import { FocusMode } from '../../../../platform/native/common/native.js'; import { IWindowOpenable, IOpenWindowOptions, IOpenEmptyWindowOptions, IPoint, IRectangle } from '../../../../platform/window/common/window.js'; export const IHostService = createDecorator('hostService'); @@ -46,13 +47,9 @@ export interface IHostService { /** * Attempt to bring the window to the foreground and focus it. * - * @param options Pass `force: true` if you want to make the window take - * focus even if the application does not have focus currently. This option - * should only be used if it is necessary to steal focus from the current - * focused application which may not be VSCode. It may not be supported - * in all environments. + * @param options How to focus the window, defaults to {@link FocusMode.Transfer} */ - focus(targetWindow: Window, options?: { force: boolean }): Promise; + focus(targetWindow: Window, options?: { mode?: FocusMode }): Promise; //#endregion diff --git a/src/vs/workbench/services/host/electron-sandbox/nativeHostService.ts b/src/vs/workbench/services/host/electron-sandbox/nativeHostService.ts index 4a8f261d50d..f3d99829c08 100644 --- a/src/vs/workbench/services/host/electron-sandbox/nativeHostService.ts +++ b/src/vs/workbench/services/host/electron-sandbox/nativeHostService.ts @@ -5,7 +5,7 @@ import { Emitter, Event } from '../../../../base/common/event.js'; import { IHostService } from '../browser/host.js'; -import { INativeHostService } from '../../../../platform/native/common/native.js'; +import { FocusMode, INativeHostService } from '../../../../platform/native/common/native.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { ILabelService, Verbosity } from '../../../../platform/label/common/label.js'; import { IWorkbenchEnvironmentService } from '../../environment/common/environmentService.js'; @@ -166,9 +166,9 @@ class WorkbenchHostService extends Disposable implements IHostService { //#region Lifecycle - focus(targetWindow: Window, options?: { force: boolean }): Promise { + focus(targetWindow: Window, options?: { mode?: FocusMode }): Promise { return this.nativeHostService.focusWindow({ - force: options?.force, + mode: options?.mode, targetWindowId: getWindowId(targetWindow) }); } diff --git a/src/vs/workbench/services/notification/common/notificationService.ts b/src/vs/workbench/services/notification/common/notificationService.ts index 24eeff23dc3..1ad6ae5eced 100644 --- a/src/vs/workbench/services/notification/common/notificationService.ts +++ b/src/vs/workbench/services/notification/common/notificationService.ts @@ -18,17 +18,21 @@ export class NotificationService extends Disposable implements INotificationServ readonly model = this._register(new NotificationsModel()); - private readonly _onDidAddNotification = this._register(new Emitter()); - readonly onDidAddNotification = this._onDidAddNotification.event; - - private readonly _onDidRemoveNotification = this._register(new Emitter()); - readonly onDidRemoveNotification = this._onDidRemoveNotification.event; - constructor( @IStorageService private readonly storageService: IStorageService ) { super(); + this.mapSourceToFilter = (() => { + const map = new Map(); + + for (const sourceFilter of this.storageService.getObject(NotificationService.PER_SOURCE_FILTER_SETTINGS_KEY, StorageScope.APPLICATION, [])) { + map.set(sourceFilter.id, sourceFilter); + } + + return map; + })(); + this.globalFilterEnabled = this.storageService.getBoolean(NotificationService.GLOBAL_FILTER_SETTINGS_KEY, StorageScope.APPLICATION, false); this.updateFilters(); @@ -38,35 +42,18 @@ export class NotificationService extends Disposable implements INotificationServ private registerListeners(): void { this._register(this.model.onDidChangeNotification(e => { switch (e.kind) { - case NotificationChangeType.ADD: - case NotificationChangeType.REMOVE: { + case NotificationChangeType.ADD: { const source = typeof e.item.sourceId === 'string' && typeof e.item.source === 'string' ? { id: e.item.sourceId, label: e.item.source } : e.item.source; - const notification: INotification = { - message: e.item.message.original, - severity: e.item.severity, - source, - priority: e.item.priority - }; + // Make sure to track sources for notifications by registering + // them with our do not disturb system which is backed by storage - if (e.kind === NotificationChangeType.ADD) { - - // Make sure to track sources for notifications by registering - // them with our do not disturb system which is backed by storage - - if (isNotificationSource(source)) { - if (!this.mapSourceToFilter.has(source.id)) { - this.setFilter({ ...source, filter: NotificationsFilter.OFF }); - } else { - this.updateSourceFilter(source); - } + if (isNotificationSource(source)) { + if (!this.mapSourceToFilter.has(source.id)) { + this.setFilter({ ...source, filter: NotificationsFilter.OFF }); + } else { + this.updateSourceFilter(source); } - - this._onDidAddNotification.fire(notification); - } - - if (e.kind === NotificationChangeType.REMOVE) { - this._onDidRemoveNotification.fire(notification); } break; @@ -85,15 +72,7 @@ export class NotificationService extends Disposable implements INotificationServ private globalFilterEnabled: boolean; - private readonly mapSourceToFilter: Map = (() => { - const map = new Map(); - - for (const sourceFilter of this.storageService.getObject(NotificationService.PER_SOURCE_FILTER_SETTINGS_KEY, StorageScope.APPLICATION, [])) { - map.set(sourceFilter.id, sourceFilter); - } - - return map; - })(); + private readonly mapSourceToFilter: Map; setFilter(filter: NotificationsFilter | INotificationSourceFilter): void { if (typeof filter === 'number') { diff --git a/src/vs/workbench/services/preferences/browser/preferencesService.ts b/src/vs/workbench/services/preferences/browser/preferencesService.ts index 565c4dd6d03..de3ec68e516 100644 --- a/src/vs/workbench/services/preferences/browser/preferencesService.ts +++ b/src/vs/workbench/services/preferences/browser/preferencesService.ts @@ -33,7 +33,7 @@ import { GroupDirection, IEditorGroup, IEditorGroupsService } from '../../editor import { IEditorService, SIDE_GROUP } from '../../editor/common/editorService.js'; import { KeybindingsEditorInput } from './keybindingsEditorInput.js'; import { DEFAULT_SETTINGS_EDITOR_SETTING, FOLDER_SETTINGS_PATH, IKeybindingsEditorPane, IOpenKeybindingsEditorOptions, IOpenSettingsOptions, IPreferencesEditorModel, IPreferencesService, ISetting, ISettingsEditorOptions, ISettingsGroup, SETTINGS_AUTHORITY, USE_SPLIT_JSON_SETTING, validateSettingsEditorOptions } from '../common/preferences.js'; -import { SettingsEditor2Input } from '../common/preferencesEditorInput.js'; +import { PreferencesEditorInput, SettingsEditor2Input } from '../common/preferencesEditorInput.js'; import { defaultKeybindingsContents, DefaultKeybindingsEditorModel, DefaultRawSettingsEditorModel, DefaultSettings, DefaultSettingsEditorModel, Settings2EditorModel, SettingsEditorModel, WorkspaceConfigurationEditorModel } from '../common/preferencesModels.js'; import { IRemoteAgentService } from '../../remote/common/remoteAgentService.js'; import { ITextEditorService } from '../../textfile/common/textEditorService.js'; @@ -214,6 +214,10 @@ export class PreferencesService extends Disposable implements IPreferencesServic return this.configurationService.getValue('workbench.settings.editor') === 'json'; } + async openPreferences(): Promise { + await this.editorGroupService.activeGroup.openEditor(this.instantiationService.createInstance(PreferencesEditorInput)); + } + openSettings(options: IOpenSettingsOptions = {}): Promise { options = { ...options, diff --git a/src/vs/workbench/services/preferences/common/preferences.ts b/src/vs/workbench/services/preferences/common/preferences.ts index 0a4441ef6f5..d67b38d8d4e 100644 --- a/src/vs/workbench/services/preferences/common/preferences.ts +++ b/src/vs/workbench/services/preferences/common/preferences.ts @@ -262,6 +262,8 @@ export interface IPreferencesService { hasDefaultSettingsContent(uri: URI): boolean; createSettings2EditorModel(): Settings2EditorModel; // TODO + openPreferences(): Promise; + openRawDefaultSettings(): Promise; openSettings(options?: IOpenSettingsOptions): Promise; openApplicationSettings(options?: IOpenSettingsOptions): Promise; diff --git a/src/vs/workbench/services/preferences/common/preferencesEditorInput.ts b/src/vs/workbench/services/preferences/common/preferencesEditorInput.ts index 21000b0a316..56b36e15fc8 100644 --- a/src/vs/workbench/services/preferences/common/preferencesEditorInput.ts +++ b/src/vs/workbench/services/preferences/common/preferencesEditorInput.ts @@ -60,3 +60,35 @@ export class SettingsEditor2Input extends EditorInput { super.dispose(); } } + +const PreferencesEditorIcon = registerIcon('preferences-editor-label-icon', Codicon.settings, nls.localize('preferencesEditorLabelIcon', 'Icon of the preferences editor label.')); + +export class PreferencesEditorInput extends EditorInput { + + static readonly ID: string = 'workbench.input.preferences'; + + readonly resource: URI = URI.from({ + scheme: Schemas.vscodeSettings, + path: `preferenceseditor` + }); + + override matches(otherInput: EditorInput | IUntypedEditorInput): boolean { + return super.matches(otherInput) || otherInput instanceof PreferencesEditorInput; + } + + override get typeId(): string { + return PreferencesEditorInput.ID; + } + + override getName(): string { + return nls.localize('preferencesEditorInputName', "Preferences"); + } + + override getIcon(): ThemeIcon { + return PreferencesEditorIcon; + } + + override async resolve(): Promise { + return null; + } +} diff --git a/src/vs/workbench/services/search/common/search.ts b/src/vs/workbench/services/search/common/search.ts index 923021734e0..5bb8115a32b 100644 --- a/src/vs/workbench/services/search/common/search.ts +++ b/src/vs/workbench/services/search/common/search.ts @@ -416,6 +416,12 @@ export const enum SearchSortOrder { CountAscending = 'countAscending' } +export const enum SemanticSearchBehavior { + Auto = 'auto', + Manual = 'manual', + RunOnEmpty = 'runOnEmpty', +} + export interface ISearchConfigurationProperties { exclude: glob.IExpression; useRipgrep: boolean; @@ -461,6 +467,10 @@ export interface ISearchConfigurationProperties { experimental: { closedNotebookRichContentResults: boolean; }; + searchView: { + semanticSearchBehavior: string; + keywordSuggestions: boolean; + }; } export interface ISearchConfiguration extends IFilesConfiguration { diff --git a/src/vs/workbench/services/textMate/browser/backgroundTokenization/textMateWorkerTokenizerController.ts b/src/vs/workbench/services/textMate/browser/backgroundTokenization/textMateWorkerTokenizerController.ts index 2e96a0eddd6..943bd396c03 100644 --- a/src/vs/workbench/services/textMate/browser/backgroundTokenization/textMateWorkerTokenizerController.ts +++ b/src/vs/workbench/services/textMate/browser/backgroundTokenization/textMateWorkerTokenizerController.ts @@ -7,7 +7,6 @@ import { importAMDNodeModule } from '../../../../../amdX.js'; import { Disposable } from '../../../../../base/common/lifecycle.js'; import { IObservable, autorun, keepObserved } from '../../../../../base/common/observable.js'; import { Proxied } from '../../../../../base/common/worker/webWorker.js'; -import { countEOL } from '../../../../../editor/common/core/misc/eolCounter.js'; import { LineRange } from '../../../../../editor/common/core/ranges/lineRange.js'; import { Range } from '../../../../../editor/common/core/range.js'; import { IBackgroundTokenizationStore, ILanguageIdCodec } from '../../../../../editor/common/languages.js'; @@ -17,23 +16,24 @@ import { IModelContentChange, IModelContentChangedEvent } from '../../../../../e import { ContiguousMultilineTokensBuilder } from '../../../../../editor/common/tokens/contiguousMultilineTokensBuilder.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { observableConfigValue } from '../../../../../platform/observable/common/platformObservableUtils.js'; -import { ArrayEdit, MonotonousIndexTransformer, SingleArrayEdit } from '../arrayOperation.js'; +import { MonotonousIndexTransformer } from '../indexTransformer.js'; import type { StateDeltas, TextMateTokenizationWorker } from './worker/textMateTokenizationWorker.worker.js'; import type { applyStateStackDiff, StateStack } from 'vscode-textmate'; +import { linesLengthEditFromModelContentChange } from '../../../../../editor/common/model/textModelStringEdit.js'; export class TextMateWorkerTokenizerController extends Disposable { private static _id = 0; - public readonly controllerId = TextMateWorkerTokenizerController._id++; - private readonly _pendingChanges: IModelContentChangedEvent[] = []; + public readonly controllerId; + private readonly _pendingChanges: IModelContentChangedEvent[]; /** * These states will eventually equal the worker states. * _states[i] stores the state at the end of line number i+1. */ - private readonly _states = new TokenizationStateStore(); + private readonly _states; - private readonly _loggingEnabled = observableConfigValue('editor.experimental.asyncTokenizationLogging', false, this._configurationService); + private readonly _loggingEnabled; private _applyStateStackDiffFn?: typeof applyStateStackDiff; private _initialState?: StateStack; @@ -47,6 +47,10 @@ export class TextMateWorkerTokenizerController extends Disposable { private readonly _maxTokenizationLineLength: IObservable, ) { super(); + this.controllerId = TextMateWorkerTokenizerController._id++; + this._pendingChanges = []; + this._states = new TokenizationStateStore(); + this._loggingEnabled = observableConfigValue('editor.experimental.asyncTokenizationLogging', false, this._configurationService); this._register(keepObserved(this._loggingEnabled)); @@ -147,7 +151,7 @@ export class TextMateWorkerTokenizerController extends Disposable { } const curToFutureTransformerTokens = MonotonousIndexTransformer.fromMany( - this._pendingChanges.map((c) => fullLineArrayEditFromModelContentChange(c.changes)) + this._pendingChanges.map((c) => linesLengthEditFromModelContentChange(c.changes)) ); // Filter tokens in lines that got changed in the future to prevent flickering @@ -176,7 +180,7 @@ export class TextMateWorkerTokenizerController extends Disposable { } const curToFutureTransformerStates = MonotonousIndexTransformer.fromMany( - this._pendingChanges.map((c) => fullLineArrayEditFromModelContentChange(c.changes)) + this._pendingChanges.map((c) => linesLengthEditFromModelContentChange(c.changes)) ); if (!this._applyStateStackDiffFn || !this._initialState) { @@ -221,21 +225,6 @@ export class TextMateWorkerTokenizerController extends Disposable { } -function fullLineArrayEditFromModelContentChange(c: IModelContentChange[]): ArrayEdit { - return new ArrayEdit( - c.map( - (c) => - new SingleArrayEdit( - c.range.startLineNumber - 1, - // Expand the edit range to include the entire line - c.range.endLineNumber - c.range.startLineNumber + 1, - countEOL(c.text)[0] + 1 - ) - ) - ); -} - function changesToString(changes: IModelContentChange[]): string { return changes.map(c => Range.lift(c.range).toString() + ' => ' + c.text).join(' & '); } - diff --git a/src/vs/workbench/services/textMate/browser/arrayOperation.ts b/src/vs/workbench/services/textMate/browser/indexTransformer.ts similarity index 55% rename from src/vs/workbench/services/textMate/browser/arrayOperation.ts rename to src/vs/workbench/services/textMate/browser/indexTransformer.ts index cb3f9e7dd21..7d229aaccd7 100644 --- a/src/vs/workbench/services/textMate/browser/arrayOperation.ts +++ b/src/vs/workbench/services/textMate/browser/indexTransformer.ts @@ -3,39 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { compareBy, numberComparator } from '../../../../base/common/arrays.js'; - -export class ArrayEdit { - public readonly edits: readonly SingleArrayEdit[]; - - constructor( - /** - * Disjoint edits that are applied in parallel - */ - edits: readonly SingleArrayEdit[] - ) { - this.edits = edits.slice().sort(compareBy(c => c.offset, numberComparator)); - } - - applyToArray(array: any[]): void { - for (let i = this.edits.length - 1; i >= 0; i--) { - const c = this.edits[i]; - array.splice(c.offset, c.length, ...new Array(c.newLength)); - } - } -} - -export class SingleArrayEdit { - constructor( - public readonly offset: number, - public readonly length: number, - public readonly newLength: number, - ) { } - - toString() { - return `[${this.offset}, +${this.length}) -> +${this.newLength}}`; - } -} +import { AnyEdit } from '../../../../editor/common/core/edits/edit.js'; export interface IIndexTransformer { transform(index: number): number | undefined; @@ -45,7 +13,7 @@ export interface IIndexTransformer { * Can only be called with increasing values of `index`. */ export class MonotonousIndexTransformer implements IIndexTransformer { - public static fromMany(transformations: ArrayEdit[]): IIndexTransformer { + public static fromMany(transformations: AnyEdit[]): IIndexTransformer { // TODO improve performance by combining transformations first const transformers = transformations.map(t => new MonotonousIndexTransformer(t)); return new CombinedIndexTransformer(transformers); @@ -54,22 +22,22 @@ export class MonotonousIndexTransformer implements IIndexTransformer { private idx = 0; private offset = 0; - constructor(private readonly transformation: ArrayEdit) { + constructor(private readonly transformation: AnyEdit) { } /** * Precondition: index >= previous-value-of(index). */ transform(index: number): number | undefined { - let nextChange = this.transformation.edits[this.idx] as SingleArrayEdit | undefined; - while (nextChange && nextChange.offset + nextChange.length <= index) { - this.offset += nextChange.newLength - nextChange.length; + let nextChange = this.transformation.replacements.at(this.idx); + while (nextChange && nextChange.replaceRange.endExclusive <= index) { + this.offset += nextChange.getLengthDelta(); this.idx++; - nextChange = this.transformation.edits[this.idx]; + nextChange = this.transformation.replacements.at(this.idx); } // assert nextChange === undefined || index < nextChange.offset + nextChange.length - if (nextChange && nextChange.offset <= index) { + if (nextChange && nextChange.replaceRange.start <= index) { // Offset is touched by the change return undefined; } diff --git a/src/vs/workbench/services/textMate/browser/textMateTokenizationFeatureImpl.ts b/src/vs/workbench/services/textMate/browser/textMateTokenizationFeatureImpl.ts index 844f240bddb..304c7c18d0b 100644 --- a/src/vs/workbench/services/textMate/browser/textMateTokenizationFeatureImpl.ts +++ b/src/vs/workbench/services/textMate/browser/textMateTokenizationFeatureImpl.ts @@ -44,22 +44,18 @@ export class TextMateTokenizationFeature extends Disposable implements ITextMate public _serviceBrand: undefined; private readonly _styleElement: HTMLStyleElement; - private readonly _createdModes: string[] = []; - private readonly _encounteredLanguages: boolean[] = []; + private readonly _createdModes: string[]; + private readonly _encounteredLanguages: boolean[]; - private _debugMode: boolean = false; - private _debugModePrintFunc: (str: string) => void = () => { }; + private _debugMode: boolean; + private _debugModePrintFunc: (str: string) => void; - private _grammarDefinitions: IValidGrammarDefinition[] | null = null; - private _grammarFactory: TMGrammarFactory | null = null; - private readonly _tokenizersRegistrations = this._register(new DisposableStore()); - private _currentTheme: IRawTheme | null = null; - private _currentTokenColorMap: string[] | null = null; - private readonly _threadedBackgroundTokenizerFactory = this._instantiationService.createInstance( - ThreadedBackgroundTokenizerFactory, - (timeMs, languageId, sourceExtensionId, lineLength, isRandomSample) => this._reportTokenizationTime(timeMs, languageId, sourceExtensionId, lineLength, true, isRandomSample), - () => this.getAsyncTokenizationEnabled(), - ); + private _grammarDefinitions: IValidGrammarDefinition[] | null; + private _grammarFactory: TMGrammarFactory | null; + private readonly _tokenizersRegistrations; + private _currentTheme: IRawTheme | null; + private _currentTokenColorMap: string[] | null; + private readonly _threadedBackgroundTokenizerFactory; constructor( @ILanguageService private readonly _languageService: ILanguageService, @@ -74,6 +70,21 @@ export class TextMateTokenizationFeature extends Disposable implements ITextMate @ITelemetryService private readonly _telemetryService: ITelemetryService, ) { super(); + this._createdModes = []; + this._encounteredLanguages = []; + this._debugMode = false; + this._debugModePrintFunc = () => { }; + this._grammarDefinitions = null; + this._grammarFactory = null; + this._tokenizersRegistrations = this._register(new DisposableStore()); + this._currentTheme = null; + this._currentTokenColorMap = null; + this._threadedBackgroundTokenizerFactory = this._instantiationService.createInstance( + ThreadedBackgroundTokenizerFactory, + (timeMs, languageId, sourceExtensionId, lineLength, isRandomSample) => this._reportTokenizationTime(timeMs, languageId, sourceExtensionId, lineLength, true, isRandomSample), + () => this.getAsyncTokenizationEnabled(), + ); + this._vscodeOniguruma = null; this._styleElement = domStylesheets.createStyleSheet(); this._styleElement.className = 'vscode-tokens-styles'; @@ -354,7 +365,7 @@ export class TextMateTokenizationFeature extends Disposable implements ITextMate return grammar; } - private _vscodeOniguruma: Promise | null = null; + private _vscodeOniguruma: Promise | null; private _getVSCodeOniguruma(): Promise { if (!this._vscodeOniguruma) { this._vscodeOniguruma = (async () => { diff --git a/src/vs/workbench/services/textMate/test/browser/arrayOperation.test.ts b/src/vs/workbench/services/textMate/test/browser/arrayOperation.test.ts index 4b5ddb6467a..17d0f1157ac 100644 --- a/src/vs/workbench/services/textMate/test/browser/arrayOperation.test.ts +++ b/src/vs/workbench/services/textMate/test/browser/arrayOperation.test.ts @@ -5,7 +5,8 @@ import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; -import { ArrayEdit, MonotonousIndexTransformer, SingleArrayEdit } from '../../browser/arrayOperation.js'; +import { MonotonousIndexTransformer } from '../../browser/indexTransformer.js'; +import { LengthEdit, LengthReplacement } from '../../../../../editor/common/core/edits/lengthEdit.js'; suite('array operation', () => { function seq(start: number, end: number) { @@ -17,16 +18,14 @@ suite('array operation', () => { } test('simple', () => { - const edit = new ArrayEdit([ - new SingleArrayEdit(4, 3, 2), - new SingleArrayEdit(8, 0, 2), - new SingleArrayEdit(9, 2, 0), + const edit = LengthEdit.create([ + LengthReplacement.create(4, 7, 2), + LengthReplacement.create(8, 8, 2), + LengthReplacement.create(9, 11, 0), ]); const arr = seq(0, 15).map(x => `item${x}`); - const newArr = arr.slice(); - - edit.applyToArray(newArr); + const newArr = edit.applyArray(arr, undefined); assert.deepStrictEqual(newArr, [ 'item0', 'item1', diff --git a/src/vs/workbench/services/url/electron-sandbox/urlService.ts b/src/vs/workbench/services/url/electron-sandbox/urlService.ts index da45a183502..c8e5aefa6e7 100644 --- a/src/vs/workbench/services/url/electron-sandbox/urlService.ts +++ b/src/vs/workbench/services/url/electron-sandbox/urlService.ts @@ -12,7 +12,7 @@ import { matchesScheme } from '../../../../base/common/network.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { ProxyChannel } from '../../../../base/parts/ipc/common/ipc.js'; -import { INativeHostService } from '../../../../platform/native/common/native.js'; +import { FocusMode, INativeHostService } from '../../../../platform/native/common/native.js'; import { NativeURLService } from '../../../../platform/url/common/urlService.js'; import { ILogService } from '../../../../platform/log/common/log.js'; @@ -71,7 +71,7 @@ export class RelayURLService extends NativeURLService implements IURLHandler, IO if (result) { this.logService.trace('URLService#handleURL(): handled', uri.toString(true)); - await this.nativeHostService.focusWindow({ force: true /* Application may not be active */, targetWindowId: this.nativeHostService.windowId }); + await this.nativeHostService.focusWindow({ mode: FocusMode.Force /* Application may not be active */, targetWindowId: this.nativeHostService.windowId }); } else { this.logService.trace('URLService#handleURL(): not handled', uri.toString(true)); } diff --git a/src/vs/workbench/test/common/notifications.test.ts b/src/vs/workbench/test/common/notifications.test.ts index 80cb08bf05f..17b4a85f173 100644 --- a/src/vs/workbench/test/common/notifications.test.ts +++ b/src/vs/workbench/test/common/notifications.test.ts @@ -8,8 +8,6 @@ import { NotificationsModel, NotificationViewItem, INotificationChangeEvent, Not import { Action } from '../../../base/common/actions.js'; import { INotification, Severity, NotificationsFilter, NotificationPriority } from '../../../platform/notification/common/notification.js'; import { createErrorWithActions } from '../../../base/common/errorMessage.js'; -import { NotificationService } from '../../services/notification/common/notificationService.js'; -import { TestStorageService } from './workbenchTestServices.js'; import { timeout } from '../../../base/common/async.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../base/test/common/utils.js'; import { DisposableStore } from '../../../base/common/lifecycle.js'; @@ -294,44 +292,5 @@ suite('Notifications', () => { item3Handle.close(); }); - test('Service', async () => { - const service = disposables.add(new NotificationService(disposables.add(new TestStorageService()))); - - let addNotificationCount = 0; - let notification!: INotification; - disposables.add(service.onDidAddNotification(n => { - addNotificationCount++; - notification = n; - })); - service.info('hello there'); - assert.strictEqual(addNotificationCount, 1); - assert.strictEqual(notification.message, 'hello there'); - assert.strictEqual(notification.priority, NotificationPriority.DEFAULT); - assert.strictEqual(notification.source, undefined); - service.model.notifications[0].close(); - - let notificationHandle = service.notify({ message: 'important message', severity: Severity.Warning }); - assert.strictEqual(addNotificationCount, 2); - assert.strictEqual(notification.message, 'important message'); - assert.strictEqual(notification.severity, Severity.Warning); - - let removeNotificationCount = 0; - disposables.add(service.onDidRemoveNotification(n => { - removeNotificationCount++; - notification = n; - })); - notificationHandle.close(); - assert.strictEqual(removeNotificationCount, 1); - assert.strictEqual(notification.message, 'important message'); - - notificationHandle = service.notify({ priority: NotificationPriority.SILENT, message: 'test', severity: Severity.Ignore }); - assert.strictEqual(addNotificationCount, 3); - assert.strictEqual(notification.message, 'test'); - assert.strictEqual(notification.priority, NotificationPriority.SILENT); - notificationHandle.close(); - assert.strictEqual(removeNotificationCount, 2); - notificationHandle.close(); - }); - ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index 5aa887cc2d6..304973440a8 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -149,6 +149,9 @@ import { ExtensionStorageService, IExtensionStorageService } from '../platform/e import { IUserDataSyncLogService } from '../platform/userDataSync/common/userDataSync.js'; import { UserDataSyncLogService } from '../platform/userDataSync/common/userDataSyncLog.js'; import { AllowedExtensionsService } from '../platform/extensionManagement/common/allowedExtensionsService.js'; +import { IMcpGalleryService, IMcpManagementService } from '../platform/mcp/common/mcpManagement.js'; +import { McpGalleryService } from '../platform/mcp/common/mcpGalleryService.js'; +import { McpManagementService } from '../platform/mcp/common/mcpManagementService.js'; registerSingleton(IUserDataSyncLogService, UserDataSyncLogService, InstantiationType.Delayed); registerSingleton(IAllowedExtensionsService, AllowedExtensionsService, InstantiationType.Delayed); @@ -164,6 +167,8 @@ registerSingleton(IContextKeyService, ContextKeyService, InstantiationType.Delay registerSingleton(ITextResourceConfigurationService, TextResourceConfigurationService, InstantiationType.Delayed); registerSingleton(IDownloadService, DownloadService, InstantiationType.Delayed); registerSingleton(IOpenerService, OpenerService, InstantiationType.Delayed); +registerSingleton(IMcpGalleryService, McpGalleryService, InstantiationType.Delayed); +registerSingleton(IMcpManagementService, McpManagementService, InstantiationType.Delayed); //#endregion @@ -402,4 +407,5 @@ import './contrib/inlineCompletions/browser/inlineCompletions.contribution.js'; import './contrib/dropOrPasteInto/browser/dropOrPasteInto.contribution.js'; + //#endregion diff --git a/src/vs/workbench/workbench.web.main.internal.ts b/src/vs/workbench/workbench.web.main.internal.ts index 86dac29932c..460c888e390 100644 --- a/src/vs/workbench/workbench.web.main.internal.ts +++ b/src/vs/workbench/workbench.web.main.internal.ts @@ -67,7 +67,7 @@ import './services/userDataProfile/browser/userDataProfileStorageService.js'; import './services/configurationResolver/browser/configurationResolverService.js'; import '../platform/extensionResourceLoader/browser/extensionResourceLoaderService.js'; import './services/auxiliaryWindow/browser/auxiliaryWindowService.js'; -import './services/browserElements/browser/browserElementsService.js'; +import './services/browserElements/browser/webBrowserElementsService.js'; import { InstantiationType, registerSingleton } from '../platform/instantiation/common/extensions.js'; import { IAccessibilityService } from '../platform/accessibility/common/accessibility.js'; diff --git a/src/vscode-dts/vscode.proposed.authIssuers.d.ts b/src/vscode-dts/vscode.proposed.authIssuers.d.ts new file mode 100644 index 00000000000..57afe5d03b7 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.authIssuers.d.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + export interface AuthenticationProviderOptions { + /** + * When specified, this provider will be associated with these issuers. They can still contain globs + * just like their extension contribution counterparts. + */ + readonly supportedIssuers?: Uri[]; + } + + export interface AuthenticationProviderSessionOptions { + /** + * When specified, the authentication provider will use the provided issuer URL to + * authenticate the user. This is only used when a provider `supportsIssuerOverride` is set to true + */ + issuer?: Uri; + } + + export interface AuthenticationGetSessionOptions { + /** + * When specified, the authentication provider will use the provided issuer URL to + * authenticate the user. This is only used when a provider `supportsIssuerOverride` is set to true + */ + issuer?: Uri; + } +} diff --git a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts index 68be8e580b6..858527a602d 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts @@ -79,7 +79,12 @@ declare module 'vscode' { constructor(value: Uri, license: string, snippet: string); } - export type ExtendedChatResponsePart = ChatResponsePart | ChatResponseTextEditPart | ChatResponseNotebookEditPart | ChatResponseConfirmationPart | ChatResponseCodeCitationPart | ChatResponseReferencePart2 | ChatResponseMovePart | ChatResponseExtensionsPart; + export class ChatPrepareToolInvocationPart { + toolName: string; + constructor(toolName: string); + } + + export type ExtendedChatResponsePart = ChatResponsePart | ChatResponseTextEditPart | ChatResponseNotebookEditPart | ChatResponseConfirmationPart | ChatResponseCodeCitationPart | ChatResponseReferencePart2 | ChatResponseMovePart | ChatResponseExtensionsPart | ChatPrepareToolInvocationPart; export class ChatResponseWarningPart { value: MarkdownString; @@ -217,6 +222,8 @@ declare module 'vscode' { codeCitation(value: Uri, license: string, snippet: string): void; + prepareToolInvocation(toolName: string): void; + push(part: ExtendedChatResponsePart): void; } diff --git a/src/vscode-dts/vscode.proposed.defaultChatParticipant.d.ts b/src/vscode-dts/vscode.proposed.defaultChatParticipant.d.ts index 830ae96509a..9f13e6f0739 100644 --- a/src/vscode-dts/vscode.proposed.defaultChatParticipant.d.ts +++ b/src/vscode-dts/vscode.proposed.defaultChatParticipant.d.ts @@ -13,10 +13,6 @@ declare module 'vscode' { message: MarkdownString; } - export interface ChatWelcomeMessageProvider { - provideSampleQuestions?(location: ChatLocation, token: CancellationToken): ProviderResult; - } - export interface ChatRequesterInformation { name: string; @@ -49,7 +45,6 @@ declare module 'vscode' { */ helpTextPostfix?: string | MarkdownString; - welcomeMessageProvider?: ChatWelcomeMessageProvider; additionalWelcomeMessage?: string | MarkdownString; titleProvider?: ChatTitleProvider; requester?: ChatRequesterInformation; diff --git a/src/vscode-dts/vscode.proposed.notebookCellExecutionState.d.ts b/src/vscode-dts/vscode.proposed.notebookCellExecutionState.d.ts deleted file mode 100644 index 67e5c4b0fbf..00000000000 --- a/src/vscode-dts/vscode.proposed.notebookCellExecutionState.d.ts +++ /dev/null @@ -1,52 +0,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 'vscode' { - - // https://github.com/microsoft/vscode/issues/124970 - - /** - * The execution state of a notebook cell. - */ - export enum NotebookCellExecutionState { - /** - * The cell is idle. - */ - Idle = 1, - /** - * Execution for the cell is pending. - */ - Pending = 2, - /** - * The cell is currently executing. - */ - Executing = 3, - } - - /** - * An event describing a cell execution state change. - */ - export interface NotebookCellExecutionStateChangeEvent { - /** - * The {@link NotebookCell cell} for which the execution state has changed. - */ - readonly cell: NotebookCell; - - /** - * The new execution state of the cell. - */ - readonly state: NotebookCellExecutionState; - } - - export namespace notebooks { - - /** - * An {@link Event} which fires when the execution state of a cell has changed. - */ - // todo@API this is an event that is fired for a property that cells don't have and that makes me wonder - // how a correct consumer works, e.g the consumer could have been late and missed an event? - export const onDidChangeNotebookCellExecutionState: Event; - } -} diff --git a/test/automation/package-lock.json b/test/automation/package-lock.json index 6bf5b31ed49..045a299e96c 100644 --- a/test/automation/package-lock.json +++ b/test/automation/package-lock.json @@ -16,7 +16,7 @@ }, "devDependencies": { "@types/ncp": "2.0.1", - "@types/node": "20.x", + "@types/node": "22.x", "@types/tmp": "0.2.2", "cpx2": "3.0.0", "nodemon": "^3.1.9", @@ -33,12 +33,13 @@ } }, "node_modules/@types/node": { - "version": "20.11.24", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.24.tgz", - "integrity": "sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long==", + "version": "22.13.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", + "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.20.0" } }, "node_modules/@types/tmp": { @@ -1263,10 +1264,11 @@ "license": "MIT" }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" }, "node_modules/universalify": { "version": "2.0.0", diff --git a/test/automation/package.json b/test/automation/package.json index cf8f9c861cb..0936e79d40e 100644 --- a/test/automation/package.json +++ b/test/automation/package.json @@ -25,7 +25,7 @@ }, "devDependencies": { "@types/ncp": "2.0.1", - "@types/node": "20.x", + "@types/node": "22.x", "@types/tmp": "0.2.2", "cpx2": "3.0.0", "nodemon": "^3.1.9", diff --git a/test/automation/src/editors.ts b/test/automation/src/editors.ts index e147d3b69ec..b2321e947de 100644 --- a/test/automation/src/editors.ts +++ b/test/automation/src/editors.ts @@ -31,7 +31,7 @@ export class Editors { await this.code.waitAndClick(`.tabs-container div.tab[data-resource-name$="${fileName}"]`); try { - await this.code.sendKeybinding(process.platform === 'darwin' ? 'cmd+1' : 'ctrl+1', () => this.waitForEditorFocus(fileName, 50 /* 50 retries * 100ms delay = 5s */)); + await this.waitForEditorFocus(fileName, 5 /* 5 retries * 100ms delay = 0.5s */); return; } catch (e) { error = e; diff --git a/test/automation/src/quickaccess.ts b/test/automation/src/quickaccess.ts index da838493d6b..731af196ee1 100644 --- a/test/automation/src/quickaccess.ts +++ b/test/automation/src/quickaccess.ts @@ -149,7 +149,9 @@ export class QuickAccess { await this.code.sendKeybinding(process.platform === 'darwin' ? 'cmd+shift+o' : 'ctrl+shift+o', accept); break; case QuickAccessKind.Commands: - await this.code.sendKeybinding(process.platform === 'darwin' ? 'cmd+shift+p' : 'ctrl+shift+p', accept); + await this.code.sendKeybinding(process.platform === 'darwin' ? 'cmd+shift+p' : 'ctrl+shift+p'); + await this.code.wait(100); + await this.quickInput.waitForQuickInputOpened(10); break; } break; diff --git a/test/automation/src/settings.ts b/test/automation/src/settings.ts index 319f4b0b236..f4a8afb3165 100644 --- a/test/automation/src/settings.ts +++ b/test/automation/src/settings.ts @@ -24,6 +24,7 @@ export class SettingsEditor { async addUserSetting(setting: string, value: string): Promise { await this.openUserSettingsFile(); + await this.editors.selectTab('settings.json'); await this.code.sendKeybinding('right', () => this.editor.waitForEditorSelection('settings.json', (s) => this._acceptEditorSelection(this.code.quality, s))); await this.editor.waitForTypeInEditor('settings.json', `"${setting}": ${value},`); @@ -39,6 +40,7 @@ export class SettingsEditor { async addUserSettings(settings: [key: string, value: string][]): Promise { await this.openUserSettingsFile(); + await this.editors.selectTab('settings.json'); await this.code.sendKeybinding('right', () => this.editor.waitForEditorSelection('settings.json', (s) => this._acceptEditorSelection(this.code.quality, s))); await this.editor.waitForTypeInEditor('settings.json', settings.map(v => `"${v[0]}": ${v[1]},`).join('')); diff --git a/test/automation/src/task.ts b/test/automation/src/task.ts index 74ffa3f5ff0..9a7033096d6 100644 --- a/test/automation/src/task.ts +++ b/test/automation/src/task.ts @@ -34,17 +34,16 @@ export class Task { } async assertTasks(filter: string, expected: ITaskConfigurationProperties[], type: 'run' | 'configure') { - await this.code.sendKeybinding('right'); // TODO https://github.com/microsoft/vscode/issues/242535 - await wait(100); - await this.editors.saveOpenedFile(); + // Artificial delay before running non hidden tasks + // so that entries from configureTask show up in the quick access. + await wait(1000); type === 'run' ? await this.quickaccess.runCommand('workbench.action.tasks.runTask', { keepOpen: true }) : await this.quickaccess.runCommand('workbench.action.tasks.configureTask', { keepOpen: true }); if (expected.length === 0) { await this.quickinput.waitForQuickInputElements(e => e.length > 1 && e.every(label => label.trim() !== filter.trim())); - } else { - await this.quickinput.waitForQuickInputElements(e => e.length > 1 && e.some(label => label.trim() === filter.trim())); } if (expected.length > 0 && !expected[0].hide) { + await this.quickinput.waitForQuickInputElements(e => e.length > 1 && e.some(label => label.trim() === filter.trim())); // select the expected task await this.quickinput.selectQuickInputElement(0, true); // Continue without scanning the output diff --git a/test/integration/browser/package-lock.json b/test/integration/browser/package-lock.json index bdec916fb4b..811c9c41054 100644 --- a/test/integration/browser/package-lock.json +++ b/test/integration/browser/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.0", "license": "MIT", "devDependencies": { - "@types/node": "20.x", + "@types/node": "22.x", "@types/rimraf": "^2.0.4", "@types/tmp": "0.1.0", "rimraf": "^2.6.1", @@ -42,12 +42,13 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.11.24", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.24.tgz", - "integrity": "sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long==", + "version": "22.13.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", + "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.20.0" } }, "node_modules/@types/rimraf": { @@ -206,10 +207,11 @@ } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" }, "node_modules/vscode-uri": { "version": "3.0.3", diff --git a/test/integration/browser/package.json b/test/integration/browser/package.json index 728d02763d1..2dce9946570 100644 --- a/test/integration/browser/package.json +++ b/test/integration/browser/package.json @@ -7,7 +7,7 @@ "compile": "node ../../../node_modules/typescript/bin/tsc" }, "devDependencies": { - "@types/node": "20.x", + "@types/node": "22.x", "@types/rimraf": "^2.0.4", "@types/tmp": "0.1.0", "rimraf": "^2.6.1", diff --git a/test/smoke/package-lock.json b/test/smoke/package-lock.json index ecbe39deabb..ebbf5644297 100644 --- a/test/smoke/package-lock.json +++ b/test/smoke/package-lock.json @@ -16,7 +16,7 @@ "devDependencies": { "@types/mocha": "^9.1.1", "@types/ncp": "2.0.1", - "@types/node": "20.x", + "@types/node": "22.x", "@types/node-fetch": "^2.5.10", "@types/rimraf": "3.0.2", "npm-run-all": "^4.1.5" @@ -61,12 +61,13 @@ } }, "node_modules/@types/node": { - "version": "20.11.24", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.24.tgz", - "integrity": "sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long==", + "version": "22.13.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", + "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.20.0" } }, "node_modules/@types/node-fetch": { @@ -912,10 +913,11 @@ "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" }, "node_modules/validate-npm-package-license": { "version": "3.0.4", diff --git a/test/smoke/package.json b/test/smoke/package.json index 22266dd559c..2c20954c0d9 100644 --- a/test/smoke/package.json +++ b/test/smoke/package.json @@ -18,7 +18,7 @@ "devDependencies": { "@types/mocha": "^9.1.1", "@types/ncp": "2.0.1", - "@types/node": "20.x", + "@types/node": "22.x", "@types/node-fetch": "^2.5.10", "@types/rimraf": "3.0.2", "npm-run-all": "^4.1.5" diff --git a/test/smoke/src/areas/task/task-quick-pick.test.ts b/test/smoke/src/areas/task/task-quick-pick.test.ts index e88b28670bb..120d2a0ea0e 100644 --- a/test/smoke/src/areas/task/task-quick-pick.test.ts +++ b/test/smoke/src/areas/task/task-quick-pick.test.ts @@ -33,7 +33,7 @@ export function setup(options?: { skipSuite: boolean }) { }); it('hide property - false', async () => { await task.configureTask({ type, command, label, hide: false }); - await task.assertTasks(label, [{ label }], 'run'); + await task.assertTasks(label, [{ label, hide: false }], 'run'); }); it('hide property - undefined', async () => { await task.configureTask({ type, command, label });