diff --git a/.eslint-plugin-local/code-no-deep-import-of-internal.ts b/.eslint-plugin-local/code-no-deep-import-of-internal.ts new file mode 100644 index 000000000000..3f54665b49ac --- /dev/null +++ b/.eslint-plugin-local/code-no-deep-import-of-internal.ts @@ -0,0 +1,66 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as eslint from 'eslint'; +import { join, dirname } from 'path'; +import { createImportRuleListener } from './utils'; + +export = new class implements eslint.Rule.RuleModule { + + readonly meta: eslint.Rule.RuleMetaData = { + messages: { + noDeepImportOfInternal: 'No deep import of internal modules allowed! Use a re-export from a non-internal module instead. Internal modules can only be imported by direct parents (any module in {{parentDir}}).' + }, + docs: { + url: 'https://github.com/microsoft/vscode/wiki/Source-Code-Organization' + }, + schema: [ + { + type: 'object', + additionalProperties: { + type: 'boolean' + } + } + ] + }; + + create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { + const patterns = context.options[0] as Record; + const internalModulePattern = Object.entries(patterns).map(([key, v]) => v ? key : undefined).filter(v => !!v); + const allowedPatterns = Object.entries(patterns).map(([key, v]) => !v ? key : undefined).filter(v => !!v); + + return createImportRuleListener((node, path) => { + const importerModuleDir = dirname(context.filename); + if (path[0] === '.') { + path = join(importerModuleDir, path); + } + const importedModulePath = path; + + const importerDirParts = splitParts(importerModuleDir); + const importedModuleParts = splitParts(importedModulePath); + + for (let i = 0; i < importedModuleParts.length; i++) { + if (internalModulePattern.some(p => importedModuleParts[i].match(p)) && allowedPatterns.every(p => !importedModuleParts[i].match(p))) { + const importerDirJoined = importerDirParts.join('/'); + const expectedParentDir = importedModuleParts.slice(0, i).join('/'); + if (!importerDirJoined.startsWith(expectedParentDir)) { + context.report({ + node, + messageId: 'noDeepImportOfInternal', + data: { + parentDir: expectedParentDir + } + }); + return; + } + } + } + }); + } +}; + +function splitParts(path: string): string[] { + return path.split(/\\|\//); +} diff --git a/.github/classifier.json b/.github/classifier.json index 93ca6f3293c3..c941f85491e3 100644 --- a/.github/classifier.json +++ b/.github/classifier.json @@ -285,7 +285,7 @@ "workbench-fonts": {"assign": []}, "workbench-history": {"assign": ["bpasero"]}, "workbench-hot-exit": {"assign": ["bpasero"]}, - "workbench-hover": {"assign": ["Tyriar"]}, + "workbench-hover": {"assign": ["Tyriar", "benibenj"]}, "workbench-launch": {"assign": []}, "workbench-link": {"assign": []}, "workbench-multiroot": {"assign": ["bpasero"]}, diff --git a/.npmrc b/.npmrc index 71d98e6c5834..e7b161dd4a9c 100644 --- a/.npmrc +++ b/.npmrc @@ -1,6 +1,6 @@ disturl="https://electronjs.org/headers" -target="34.3.2" -ms_build_id="11161073" +target="34.4.0" +ms_build_id="11308985" runtime="electron" build_from_source="true" legacy-peer-deps="true" diff --git a/.vscode/extensions/vscode-selfhost-test-provider/package-lock.json b/.vscode/extensions/vscode-selfhost-test-provider/package-lock.json index a71a68e4e365..342512be8a92 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/package-lock.json +++ b/.vscode/extensions/vscode-selfhost-test-provider/package-lock.json @@ -56,12 +56,13 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.12.11", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.11.tgz", - "integrity": "sha512-vDg9PZ/zi+Nqp6boSOT7plNuthRugEKixDv5sFTIpkE89MmNtEArAShI4mxuX2+UrLEe9pxC1vm2cjm9YlWbJw==", + "version": "20.17.27", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.27.tgz", + "integrity": "sha512-U58sbKhDrthHlxHRJw7ZLiLDZGmAUOZUbpw0S6nL27sYUdhvgBLCRu/keSd6qcTsfArd1sRFCCBxzWATGr/0UA==", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.2" } }, "node_modules/ansi-styles": { @@ -92,10 +93,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.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" } } } diff --git a/build/azure-pipelines/alpine/product-build-alpine.yml b/build/azure-pipelines/alpine/product-build-alpine.yml index d6fe74a9d610..95aa6fa2449b 100644 --- a/build/azure-pipelines/alpine/product-build-alpine.yml +++ b/build/azure-pipelines/alpine/product-build-alpine.yml @@ -27,7 +27,7 @@ steps: condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) displayName: Setup NPM Registry - - script: mkdir -p .build && node build/azure-pipelines/common/computeNodeModulesCacheKey.js alpine $VSCODE_ARCH > .build/packagelockhash + - script: mkdir -p .build && node build/azure-pipelines/common/computeNodeModulesCacheKey.js alpine $VSCODE_ARCH $(node -p process.arch) > .build/packagelockhash displayName: Prepare node_modules cache key - task: Cache@2 diff --git a/build/azure-pipelines/darwin/product-build-darwin.yml b/build/azure-pipelines/darwin/product-build-darwin.yml index bd37d675aa2f..1691dff3231c 100644 --- a/build/azure-pipelines/darwin/product-build-darwin.yml +++ b/build/azure-pipelines/darwin/product-build-darwin.yml @@ -45,7 +45,7 @@ steps: condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) displayName: Setup NPM Registry - - script: mkdir -p .build && node build/azure-pipelines/common/computeNodeModulesCacheKey.js darwin $VSCODE_ARCH > .build/packagelockhash + - script: mkdir -p .build && node build/azure-pipelines/common/computeNodeModulesCacheKey.js darwin $VSCODE_ARCH $(node -p process.arch) > .build/packagelockhash displayName: Prepare node_modules cache key - task: Cache@2 diff --git a/build/azure-pipelines/linux/product-build-linux.yml b/build/azure-pipelines/linux/product-build-linux.yml index b9300b1ccba0..7471979a3cc2 100644 --- a/build/azure-pipelines/linux/product-build-linux.yml +++ b/build/azure-pipelines/linux/product-build-linux.yml @@ -69,7 +69,7 @@ steps: condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) displayName: Setup NPM Registry - - script: mkdir -p .build && node build/azure-pipelines/common/computeNodeModulesCacheKey.js linux $VSCODE_ARCH > .build/packagelockhash + - script: mkdir -p .build && node build/azure-pipelines/common/computeNodeModulesCacheKey.js linux $VSCODE_ARCH $(node -p process.arch) > .build/packagelockhash displayName: Prepare node_modules cache key - task: Cache@2 diff --git a/build/azure-pipelines/oss/product-build-pr-cache-linux.yml b/build/azure-pipelines/oss/product-build-pr-cache-linux.yml index 72cd33cdd75c..b4a2cc3a4807 100644 --- a/build/azure-pipelines/oss/product-build-pr-cache-linux.yml +++ b/build/azure-pipelines/oss/product-build-pr-cache-linux.yml @@ -13,7 +13,7 @@ steps: condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) displayName: Setup NPM Registry - - script: mkdir -p .build && node build/azure-pipelines/common/computeNodeModulesCacheKey.js linux $VSCODE_ARCH > .build/packagelockhash + - script: mkdir -p .build && node build/azure-pipelines/common/computeNodeModulesCacheKey.js linux $VSCODE_ARCH $(node -p process.arch) > .build/packagelockhash displayName: Prepare node_modules cache key - task: Cache@2 diff --git a/build/azure-pipelines/oss/product-build-pr-cache-win32.yml b/build/azure-pipelines/oss/product-build-pr-cache-win32.yml index 76944f69b148..f4a82587567f 100644 --- a/build/azure-pipelines/oss/product-build-pr-cache-win32.yml +++ b/build/azure-pipelines/oss/product-build-pr-cache-win32.yml @@ -15,7 +15,7 @@ steps: - pwsh: | mkdir .build -ea 0 - node build/azure-pipelines/common/computeNodeModulesCacheKey.js win32 $(VSCODE_ARCH) > .build/packagelockhash + node build/azure-pipelines/common/computeNodeModulesCacheKey.js win32 $(VSCODE_ARCH) $(node -p process.arch) > .build/packagelockhash displayName: Prepare node_modules cache key - task: Cache@2 diff --git a/build/azure-pipelines/product-build.yml b/build/azure-pipelines/product-build.yml index bf25633b5c85..7098749bbc6a 100644 --- a/build/azure-pipelines/product-build.yml +++ b/build/azure-pipelines/product-build.yml @@ -197,8 +197,6 @@ extends: pool: name: AcesShared os: macOS - variables: - VSCODE_ARCH: arm64 steps: - template: build/azure-pipelines/product-compile.yml@self parameters: @@ -264,8 +262,7 @@ extends: - ${{ if eq(parameters.VSCODE_BUILD_MACOS, true) }}: - job: CLIMacOSX64 pool: - name: Azure Pipelines - image: macOS-13 + name: AcesShared os: macOS variables: # todo@connor4312 to diagnose build flakes @@ -281,8 +278,7 @@ extends: - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_MACOS_ARM64, true)) }}: - job: CLIMacOSARM64 pool: - name: Azure Pipelines - image: macOS-13 + name: AcesShared os: macOS variables: # todo@connor4312 to diagnose build flakes @@ -585,8 +581,7 @@ extends: - ${{ if or(eq(parameters.VSCODE_BUILD_LINUX, true),eq(parameters.VSCODE_BUILD_LINUX_ARMHF, true),eq(parameters.VSCODE_BUILD_LINUX_ARM64, true),eq(parameters.VSCODE_BUILD_ALPINE, true),eq(parameters.VSCODE_BUILD_ALPINE_ARM64, true),eq(parameters.VSCODE_BUILD_MACOS, true),eq(parameters.VSCODE_BUILD_MACOS_ARM64, true),eq(parameters.VSCODE_BUILD_WIN32, true),eq(parameters.VSCODE_BUILD_WIN32_ARM64, true)) }}: - CompileCLI pool: - name: Azure Pipelines - image: macOS-13 + name: AcesShared os: macOS variables: BUILDSECMON_OPT_IN: true @@ -596,7 +591,7 @@ extends: displayName: Unit Tests timeoutInMinutes: 90 variables: - VSCODE_ARCH: x64 + VSCODE_ARCH: arm64 steps: - template: build/azure-pipelines/darwin/product-build-darwin.yml@self parameters: @@ -609,7 +604,7 @@ extends: displayName: Integration Tests timeoutInMinutes: 90 variables: - VSCODE_ARCH: x64 + VSCODE_ARCH: arm64 steps: - template: build/azure-pipelines/darwin/product-build-darwin.yml@self parameters: @@ -622,7 +617,7 @@ extends: displayName: Smoke Tests timeoutInMinutes: 90 variables: - VSCODE_ARCH: x64 + VSCODE_ARCH: arm64 steps: - template: build/azure-pipelines/darwin/product-build-darwin.yml@self parameters: @@ -650,7 +645,7 @@ extends: - job: macOSTest timeoutInMinutes: 90 variables: - VSCODE_ARCH: x64 + VSCODE_ARCH: arm64 steps: - template: build/azure-pipelines/darwin/product-build-darwin.yml@self parameters: diff --git a/build/azure-pipelines/product-compile.yml b/build/azure-pipelines/product-compile.yml index 6096157ad8d5..fba31eefcd16 100644 --- a/build/azure-pipelines/product-compile.yml +++ b/build/azure-pipelines/product-compile.yml @@ -23,7 +23,7 @@ steps: condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) displayName: Setup NPM Registry - - script: mkdir -p .build && node build/azure-pipelines/common/computeNodeModulesCacheKey.js compile $VSCODE_ARCH > .build/packagelockhash + - script: mkdir -p .build && node build/azure-pipelines/common/computeNodeModulesCacheKey.js compile $(node -p process.arch) > .build/packagelockhash displayName: Prepare node_modules cache key - task: Cache@2 diff --git a/build/azure-pipelines/web/product-build-web.yml b/build/azure-pipelines/web/product-build-web.yml index e0e91c1c5898..3f94460dfafc 100644 --- a/build/azure-pipelines/web/product-build-web.yml +++ b/build/azure-pipelines/web/product-build-web.yml @@ -27,7 +27,7 @@ steps: condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) displayName: Setup NPM Registry - - script: mkdir -p .build && node build/azure-pipelines/common/computeNodeModulesCacheKey.js web > .build/packagelockhash + - script: mkdir -p .build && node build/azure-pipelines/common/computeNodeModulesCacheKey.js web $(node -p process.arch) > .build/packagelockhash displayName: Prepare node_modules cache key - task: Cache@2 diff --git a/build/azure-pipelines/win32/product-build-win32.yml b/build/azure-pipelines/win32/product-build-win32.yml index b10de41da59b..56bcf7a63882 100644 --- a/build/azure-pipelines/win32/product-build-win32.yml +++ b/build/azure-pipelines/win32/product-build-win32.yml @@ -57,7 +57,7 @@ steps: - pwsh: | mkdir .build -ea 0 - node build/azure-pipelines/common/computeNodeModulesCacheKey.js win32 $(VSCODE_ARCH) > .build/packagelockhash + node build/azure-pipelines/common/computeNodeModulesCacheKey.js win32 $(VSCODE_ARCH) $(node -p process.arch) > .build/packagelockhash displayName: Prepare node_modules cache key - task: Cache@2 diff --git a/build/checksums/electron.txt b/build/checksums/electron.txt index d390d7a233ca..028b100c0246 100644 --- a/build/checksums/electron.txt +++ b/build/checksums/electron.txt @@ -1,75 +1,75 @@ -c9b82c9f381742e839fea00aeb14f24519bcaf38a0f4eed25532191701f9535b *chromedriver-v34.3.2-darwin-arm64.zip -d556c1e2b06f1bf131e83c2fb981de755c28e1083a884d257eb964815be16b0c *chromedriver-v34.3.2-darwin-x64.zip -1cabad4f3303ac2ff172a9f22185f64944dbaa6fc68271609077158eaefdee35 *chromedriver-v34.3.2-linux-arm64.zip -4213ce52c72ef414179b5c5c22ae8423847ff030d438296bd6c2aac763930a7b *chromedriver-v34.3.2-linux-armv7l.zip -3c64c08221fdfc0f4be60ea8b1b126f2ecca45f60001b63778522f711022c6ea *chromedriver-v34.3.2-linux-x64.zip -e8388734d88e011cb6cd79795431de9206820749219d80565ee49d90501d2bf3 *chromedriver-v34.3.2-mas-arm64.zip -3ad1dd37bd6e0bb37e8503898db7aedd56bd5213e6d6760b05c3d11f4625062b *chromedriver-v34.3.2-mas-x64.zip -d567b481a0f5d88e84bba7718f89fb08f56363bfc4cb5914e1c2086358a5c252 *chromedriver-v34.3.2-win32-arm64.zip -df6732e9dc61cb20a3c0b2a2de453aac7e2bd54e7cbff43512afa614852c15fa *chromedriver-v34.3.2-win32-ia32.zip -dda0765c8d064924632e18cd152014ecd767f3808fc51c8249a053bfb7ca70a2 *chromedriver-v34.3.2-win32-x64.zip -1945f15caff98f2e0f1ee539c483d352fb8d4d0c13f342caa7abe247676d828c *electron-api.json -c078bbf727b3c3026f60e07a0f4643b85c06c581b54be017d0a6c284ba6772d3 *electron-v34.3.2-darwin-arm64-dsym-snapshot.zip -35f587754d6a3272606258386bf73688d63dd53c7e572d3a7cbaae6f3f60bdae *electron-v34.3.2-darwin-arm64-dsym.zip -08b14ee02c98353de3c738120dfd017322666e82b914a7f6de9b9888dcc5c0f0 *electron-v34.3.2-darwin-arm64-symbols.zip -2a4aa7e8fa30f229e465ebd18d3e4722e2b41529dc51a68a954d333a7e556ffe *electron-v34.3.2-darwin-arm64.zip -1509ccdeb80024f5e3edd5ecf804b4cef4e47ea2bd74e33ef0b39044b0ccf892 *electron-v34.3.2-darwin-x64-dsym-snapshot.zip -3bbe5d587c3f582ed8c126b0fb635cc02ad9a14d077b04892fe6f862092445b0 *electron-v34.3.2-darwin-x64-dsym.zip -fa7ece82e6ecaf1c94ed341e8ebff98e64687c68fe113f52cd9a21400302e22f *electron-v34.3.2-darwin-x64-symbols.zip -23938c62257a65a863ed7aa7c7966ba5f257a7d3dc16c78293e826962cc39c5c *electron-v34.3.2-darwin-x64.zip -0547eecf8ab538d74fa854f591ce8b888a3dbb339256d2db3038e7bb2c6dd929 *electron-v34.3.2-linux-arm64-debug.zip -676d0dc2b1c1c85c8b2abbb8cd5376ee22ecdb910493b910d9ae5a998532136a *electron-v34.3.2-linux-arm64-symbols.zip -774e4ccb39d553e5487994a9f8c60774a90f08cdb049ff65f3963fc27c969ff2 *electron-v34.3.2-linux-arm64.zip -0547eecf8ab538d74fa854f591ce8b888a3dbb339256d2db3038e7bb2c6dd929 *electron-v34.3.2-linux-armv7l-debug.zip -ba33bf53fcb35dea568a2795f5b23ecf46c218abe8258946611c72a1f42f716c *electron-v34.3.2-linux-armv7l-symbols.zip -73ae92c8fffb351d3a455569cf57ce9a3f676f42bf61939c613c607fe0fc3bfb *electron-v34.3.2-linux-armv7l.zip -e61a9a69dd7ea6f2687708a8e83516670cdea53c728226e598e2f6f1fad5b77b *electron-v34.3.2-linux-x64-debug.zip -f1a04df7fe67dd1cd29e7b87871525458d2eb24c0cf3b5835a1c56974707562a *electron-v34.3.2-linux-x64-symbols.zip -7b74c0c4fae82e27c7e9cbca13e9763e046113dba8737d3e27de9a0b300ac87e *electron-v34.3.2-linux-x64.zip -8571a6aa83e00925ceb39fdc5a45a9f6b9aa3d92fd84951c6f252ed723aea4ae *electron-v34.3.2-mas-arm64-dsym-snapshot.zip -477410c6f9a6c5eeaedf376058a02c2996fc0a334aa40eeec7d3734c09347f4d *electron-v34.3.2-mas-arm64-dsym.zip -c2e62dcd6630cb51b2d8e2869e74e47d29bda785521cea6e82e546d0fc58aabb *electron-v34.3.2-mas-arm64-symbols.zip -a1698e8546a062fd59b7f8e5507a7f3220fb00b347f2377de83fc9a07f7f3507 *electron-v34.3.2-mas-arm64.zip -741a24ac230a3651dca81d211f9f00b835c428a5ed0c5f67d370d4e88b62f8d6 *electron-v34.3.2-mas-x64-dsym-snapshot.zip -aeff97ec9e5c9e173ac89e38acd94476025c5640d5f27be1e8c2abd54398bab3 *electron-v34.3.2-mas-x64-dsym.zip -9f14b66b1d612ac66697288e8763171c388f7f200854871a5f0ab464a6a921c2 *electron-v34.3.2-mas-x64-symbols.zip -c979d7e7175f1e8e03ca187997d4c156b878189fc3611b347fadebcb16f3e027 *electron-v34.3.2-mas-x64.zip -f43c700641e8220205dd356952e32718d113cf530520c4ed7209b59851eac266 *electron-v34.3.2-win32-arm64-pdb.zip -3ba6e01c99bffac6b5dd3fd6f122ecdb571cf6f675dc5498c65050bd7a382ef8 *electron-v34.3.2-win32-arm64-symbols.zip -c23f84aabb09c24cd2ae759a547fdba4206af19a3bb0f4554a91cd9528648ad0 *electron-v34.3.2-win32-arm64-toolchain-profile.zip -9b9cb65d75a16782088b492f9ef3bb4d27525012b819c12bf29bd27e159d749b *electron-v34.3.2-win32-arm64.zip -1006e7af4c149114b5ebc3497617aaa6cd1bb0b131e0a225fd73709ff308f9c5 *electron-v34.3.2-win32-ia32-pdb.zip -1ecb6430cd04454f08f557c9579163f3552144bfcc0b67b768dad8868b5b891d *electron-v34.3.2-win32-ia32-symbols.zip -c23f84aabb09c24cd2ae759a547fdba4206af19a3bb0f4554a91cd9528648ad0 *electron-v34.3.2-win32-ia32-toolchain-profile.zip -d004fd5f853754001fafaec33e383d1950b30c935ee71b297ec1c9e084355e9b *electron-v34.3.2-win32-ia32.zip -4e0721552fd2f09e9466e88089af8b965f1bfbc4ae00a59aaf6245b1d1efabfd *electron-v34.3.2-win32-x64-pdb.zip -9dea812a7e7cd0fb18e5fed9a99db5531959a068c24d3c0ecedceb644cd3ffa0 *electron-v34.3.2-win32-x64-symbols.zip -c23f84aabb09c24cd2ae759a547fdba4206af19a3bb0f4554a91cd9528648ad0 *electron-v34.3.2-win32-x64-toolchain-profile.zip -1785e161420fb90d2331c26e50bba3413cae9625b7db3c8524ea02ade631efba *electron-v34.3.2-win32-x64.zip -722b304c31ddac58b0083d94a241c5276464f04bd8ea4fcbfd33051d197be103 *electron.d.ts -31ce159b2e47d1de5bc907d8e1c89726b0f2ba530ec2e9d7a8e5c723b1ccf6e0 *ffmpeg-v34.3.2-darwin-arm64.zip -565539bac64a6ee9cf6f188070f520210a1507341718f5dc388ac7c454b1e1d5 *ffmpeg-v34.3.2-darwin-x64.zip -6006ea0f46ab229feb2685be086b0fafd65981e2939dd2218a078459c75ab527 *ffmpeg-v34.3.2-linux-arm64.zip -9404ce2e85df7c40f809f2cf62c7af607de299839fe6b7ae978c3015500abcc8 *ffmpeg-v34.3.2-linux-armv7l.zip -79aec96898b7e2462826780ee0b52b9ab299dc662af333e128a34fd5ddae87f1 *ffmpeg-v34.3.2-linux-x64.zip -9190743c78210574faf5d5ecb82a00f8fa15e5f2253378cb925a99ca9d39961b *ffmpeg-v34.3.2-mas-arm64.zip -48915adcb1a6342efeda896035101300f0432c0304cfb38f2378e98c6309ebae *ffmpeg-v34.3.2-mas-x64.zip -745d5ef786de6d4a720475079836e2fda7b501cfcd255819485a47de5b24b74e *ffmpeg-v34.3.2-win32-arm64.zip -d0d86d60978439dc8ae4a723d4e4c1f853891d596bfd84033440a232fa762e2f *ffmpeg-v34.3.2-win32-ia32.zip -4441539fd8c9cbe79880ff1bade9bdc0c3744c33d7409130af6404e57ee401ff *ffmpeg-v34.3.2-win32-x64.zip -39edd1eeefe881aa75af0e438204e0b1c6e6724e34fa5819109276331c0c2c9a *hunspell_dictionaries.zip -20dd417536e5f4ebc01f480221284c0673729c27b082bc04e2922f16cd571719 *libcxx-objects-v34.3.2-linux-arm64.zip -7e53c5779c04f895f8282c0450ec4a63074d15a0e910e41006cfea133d0288af *libcxx-objects-v34.3.2-linux-armv7l.zip -92e2283c924ab523ffec3ea22513beaab6417f7fc3d570f57d51a1e1ceb7f510 *libcxx-objects-v34.3.2-linux-x64.zip -9bf3c6e8ad68f523fe086fada4148dd04e5bb0b9290d104873e66f2584a5cf50 *libcxx_headers.zip -34e4b44f9c5e08b557a2caed55456ce7690abab910196a783a2a47b58d2b9ac9 *libcxxabi_headers.zip -11f67635e6659f9188198e4086c51b89890b61a22f6c17c99eff35595ee8f51d *mksnapshot-v34.3.2-darwin-arm64.zip -c0add9ef4ac27c73fa043d04b4c9635fd3fd9f5c16d7a03e961864ba05252813 *mksnapshot-v34.3.2-darwin-x64.zip -6262adf86a340d8d28059937b01ef1117b93212e945fddbceea5c18d7e7c64f0 *mksnapshot-v34.3.2-linux-arm64-x64.zip -f7db8ebe91a1cc8d24ef6aad12949a18d8e4975ac296e3e5e9ecd88c9bccb143 *mksnapshot-v34.3.2-linux-armv7l-x64.zip -6642038e86bda362980ff1c8973a404e2b02efdd87de9e35b650fc1e743833da *mksnapshot-v34.3.2-linux-x64.zip -15883bf8e8cd737c3682d1e719d7cbac92f50b525681aac324dca876861dfc7d *mksnapshot-v34.3.2-mas-arm64.zip -4da23a950bfcc377ef21c37d496017ab4c36da03f3b41049ac114042c42608ce *mksnapshot-v34.3.2-mas-x64.zip -fab59573d3c2f9bdf31146a1896d24ac0c51f736aad86d2f3c7ecef13c05a7fd *mksnapshot-v34.3.2-win32-arm64-x64.zip -66f25e07c6f8d5d2009577a129440255a3baf63c929a5b60b2e77cd52e46105b *mksnapshot-v34.3.2-win32-ia32.zip -8168bfbf61882cfac80aed1e71e364e1c7f2fccd11eac298e6abade8b46894ea *mksnapshot-v34.3.2-win32-x64.zip +d725fdbc02fb506692462e123f8feb232984c653b52cdae99773b9b86aec6cc8 *chromedriver-v34.4.0-darwin-arm64.zip +8f040aac9f120dcf653777c1bf54dbd35c8593efa7834e344da9cfb666f26520 *chromedriver-v34.4.0-darwin-x64.zip +7bb480aac5702941afa7972bac6685290ee1e9784ed876a6a34f1a326c0836b1 *chromedriver-v34.4.0-linux-arm64.zip +721860fe261a6d2d45b5f7df8d258b0e96800c4848b4841c1d894366c82ce69c *chromedriver-v34.4.0-linux-armv7l.zip +def0b44b0d16ee00fdf84d45995d61a14526b1ac884b78f37ffec7520dba22e3 *chromedriver-v34.4.0-linux-x64.zip +f95956bbef533da9a654cc7f39c74eff11b95dbaeaadc3de4461c141621065ac *chromedriver-v34.4.0-mas-arm64.zip +8aae7592af5456d52b2adc2168b54448ed2d15a9cea52643096d05fd9ddbc3da *chromedriver-v34.4.0-mas-x64.zip +ab9e05b0cd86bcc17d15b9f080abff458f8d292c463bcd288e85ec39361e5126 *chromedriver-v34.4.0-win32-arm64.zip +f7e1b5397555c31e71204924e154515166d7bfa9cb1d4aae69cd8c13ae21e960 *chromedriver-v34.4.0-win32-ia32.zip +ce186aa0df40a0fdf7f2e67496341fc4a9db8cb371c6d9b48383eea751d0ba97 *chromedriver-v34.4.0-win32-x64.zip +c74db20f5ed9e1dfd3b91af98245ecd8565e54cb22d17097970d82391e2bbf12 *electron-api.json +f5d8e96a06879ce0bbcabdbdf6f678b59d9ae72aec6ab115a7355d44ddc49d70 *electron-v34.4.0-darwin-arm64-dsym-snapshot.zip +3194b1594945b2c54009274f7f4ea3f271d17b5391817e701f4297bff3b25122 *electron-v34.4.0-darwin-arm64-dsym.zip +0752b8d1f2fd8325f81a4d9663b54639249b87a9496c95a57af16e0ed0471743 *electron-v34.4.0-darwin-arm64-symbols.zip +33fd3c5300bda19350478f6e51719187d7ed5ca1830fa89f52598950d56600f2 *electron-v34.4.0-darwin-arm64.zip +85c31a47db272df0d9a4f00e876940ec1d6269171efe765806e5fc9b884cee0d *electron-v34.4.0-darwin-x64-dsym-snapshot.zip +72ce4fe3c6096500d79c7e5f591e253d5b5ccafad9efe484abdc66ddb0a0c5bd *electron-v34.4.0-darwin-x64-dsym.zip +72dd760ce34e86309762927fdf95aa7f797aadf4c74c20dc68cb2d3ecb77b23a *electron-v34.4.0-darwin-x64-symbols.zip +347ffc0beb8913a7fea2a89fc3a75acd2baea82a45df4f52c16e0df38a371374 *electron-v34.4.0-darwin-x64.zip +d79d27ddd0432ab422d6dbdbf5ce169d9f4221503c0a9f7a8dd594641602d9ee *electron-v34.4.0-linux-arm64-debug.zip +76657528fac7bed38c7bae78a3e2c9b8b9a602b2449c98b3a09050cab8cf5b3f *electron-v34.4.0-linux-arm64-symbols.zip +2da3f57accda3aada5dcf351818619b8c0de5709f83c021bba8fbf8002ff4fb8 *electron-v34.4.0-linux-arm64.zip +d79d27ddd0432ab422d6dbdbf5ce169d9f4221503c0a9f7a8dd594641602d9ee *electron-v34.4.0-linux-armv7l-debug.zip +a71fd2dcd64c5d25e9eca06bf0bc3d3b92632b42bb6a040e967572a55359dc9a *electron-v34.4.0-linux-armv7l-symbols.zip +63e0caefd697095900fb04e4c2a733d875a04240d4a3379963c9e353547a4999 *electron-v34.4.0-linux-armv7l.zip +1b36dff25f20c3c80fc497a89210974eeefeb0ec9df7c5c460d65905c1ab6494 *electron-v34.4.0-linux-x64-debug.zip +1c86992eae47ddf7ee0bc2e37abb98c67f9c9835b45664eef39ee94bc7e1a020 *electron-v34.4.0-linux-x64-symbols.zip +f1b9b91282343cd2e4dc3324f57691853713dcc0309dfb5b7bc556778207ff93 *electron-v34.4.0-linux-x64.zip +aceb61924ba0ce36a407e831c1ecdbde976766b193877f7deb1306da0975e7e7 *electron-v34.4.0-mas-arm64-dsym-snapshot.zip +ce5ca634a90d2280bcf69811ffb55262773040a17a30ab1a1fb7c44be216510d *electron-v34.4.0-mas-arm64-dsym.zip +9c3eea6198cb0fa3890e57acf6996385f6d754abbc9f6c0f5a9ff01793a498d7 *electron-v34.4.0-mas-arm64-symbols.zip +5da7499b9dabc4338b7d22fcf994235470e745f9f4f1da3c6be86dd6060c6315 *electron-v34.4.0-mas-arm64.zip +121f8a9e5ea6a65843a7b9e2bb45ccbcb90e83759ec6fe59e2b5cbd4409a439d *electron-v34.4.0-mas-x64-dsym-snapshot.zip +b7602ddcd02da9ae066acc9b4acc1152a920eee7e3863e802b219669e0e84436 *electron-v34.4.0-mas-x64-dsym.zip +873766fbb0ec114dccc5ece6a044abadfd70ddb5eb274fcc83158f17a58c022c *electron-v34.4.0-mas-x64-symbols.zip +518f1c584e2fc7d10d64efabb62eeafb886e5679ab53d3a54d56fa5220df4152 *electron-v34.4.0-mas-x64.zip +52b659fe388ad9fab0a3a0422be5eddc6396184fe960a70b31f4ff7fcdf2a9a5 *electron-v34.4.0-win32-arm64-pdb.zip +71ee022ed36ed96dd8df2102fc4e637331106ca5b7436bf2f30a543fea52d2cf *electron-v34.4.0-win32-arm64-symbols.zip +c23f84aabb09c24cd2ae759a547fdba4206af19a3bb0f4554a91cd9528648ad0 *electron-v34.4.0-win32-arm64-toolchain-profile.zip +0f92877d5a81590ca00709a3850bcdf0203dd84b3519eda65119ad1322138bbe *electron-v34.4.0-win32-arm64.zip +ba26e2f8bb0cf3efd74dafbcc7d89382e044eff460afc05ed199ce296c6e77d7 *electron-v34.4.0-win32-ia32-pdb.zip +a3a52ea3a4c73d4c8724e3e5db6d7b7d017c89479ab45ded4b8e6cb6132ae516 *electron-v34.4.0-win32-ia32-symbols.zip +c23f84aabb09c24cd2ae759a547fdba4206af19a3bb0f4554a91cd9528648ad0 *electron-v34.4.0-win32-ia32-toolchain-profile.zip +a3f2fb6b939c0e5b12fc2e15fdebee06b73e078181ec6ea1fa1d7d7d08202fa0 *electron-v34.4.0-win32-ia32.zip +90dc5412038c455773ca28130e5ab1f2dc2c6769f3996b5888bc4575f2e43e28 *electron-v34.4.0-win32-x64-pdb.zip +951d5fb3870359b0d6f63f516e86f916824c82b1f3d23edf608599abfb7a03e7 *electron-v34.4.0-win32-x64-symbols.zip +c23f84aabb09c24cd2ae759a547fdba4206af19a3bb0f4554a91cd9528648ad0 *electron-v34.4.0-win32-x64-toolchain-profile.zip +359026d5224cb536d1516a1e0052d1a4771f0402e142fe66e007613af80616d1 *electron-v34.4.0-win32-x64.zip +c2e934d5053503f048a217d9ca4507cf597306f024e8878c27dd34931c0941aa *electron.d.ts +b224dedf0b8e39d3ffe17fef4955da89a13133beaf7de739cabc39b04f6c1720 *ffmpeg-v34.4.0-darwin-arm64.zip +6dafde78730e9ad454d1d70a3cb689d81a5cd460ebd1846bba4d6771936022e2 *ffmpeg-v34.4.0-darwin-x64.zip +346101611df565cabcfaa3515b1db3f70d0891ba8f1241074dd09b69e12630d2 *ffmpeg-v34.4.0-linux-arm64.zip +5c77c712ee93bd26706daa78f0651d9b4ba8e4b46a115908f29d2742a2e1b9f0 *ffmpeg-v34.4.0-linux-armv7l.zip +f5ab70d399d528450c9499966e88ce02a368bb8c7dd7ac0676a6628fa29b3f14 *ffmpeg-v34.4.0-linux-x64.zip +fada98cd1915fc8edf185e324784a5bf2d92792f8d486b1e8c329f8590d745b6 *ffmpeg-v34.4.0-mas-arm64.zip +bd8b1b746d72757e35823d69a049272a77c96c692b491e386ff1b407af651369 *ffmpeg-v34.4.0-mas-x64.zip +dad4dc626670a651cb430f25ec99ffca36993d914bbe03ff72cd462b0b5e82ed *ffmpeg-v34.4.0-win32-arm64.zip +dad4dc626670a651cb430f25ec99ffca36993d914bbe03ff72cd462b0b5e82ed *ffmpeg-v34.4.0-win32-ia32.zip +dad4dc626670a651cb430f25ec99ffca36993d914bbe03ff72cd462b0b5e82ed *ffmpeg-v34.4.0-win32-x64.zip +2e440d189ab79dc13d750baaaaa33bfe8da250b1c0856cffe5c21ef80f5eb601 *hunspell_dictionaries.zip +6a030f9082aad0763754bd460b94af6def7c0a3276d4f807aa735bf86fdbfa0e *libcxx-objects-v34.4.0-linux-arm64.zip +fdfab7bd836b7bb5f808c2eaebaa158afed8b3ff99ba855402e9292a576ab375 *libcxx-objects-v34.4.0-linux-armv7l.zip +49aeeb0e9ff18c3624953ec2edbaff6e4e19cb3ee785efd9aee5a30913fa5d50 *libcxx-objects-v34.4.0-linux-x64.zip +bc529e4e7376a78abcd880dd8b43efd4194f431be345b0a4fc24bb2d3046b35a *libcxx_headers.zip +c46a23338c31d465ddb4005e870b955da8886146e5ee92b2bab1c8bccf876a85 *libcxxabi_headers.zip +84826b3794c096e0188a83a1c0aba3deb416e45d647cdcd21148eca78c7d9ee6 *mksnapshot-v34.4.0-darwin-arm64.zip +3125179e9923408670f9f129081f2b443ebd7d701a58814f4fd4b2eb782161f9 *mksnapshot-v34.4.0-darwin-x64.zip +3913c51aa34f122875524cfdbcd096352f8da24c631f301872aa34e119afd9d4 *mksnapshot-v34.4.0-linux-arm64-x64.zip +4790497b0015d0aedb250dbe23adb3727faadc1f057d6b6c9ecfee7820dd8fe1 *mksnapshot-v34.4.0-linux-armv7l-x64.zip +44d61f6526173959e9dcfe7eb4a0bad5af1f8d2fd4131e1a20f4436f5ab96993 *mksnapshot-v34.4.0-linux-x64.zip +6e2ddc05109204fd972bcc4ef57650c5ee75bc5d09e5a75e6c8b36e4ba8214ac *mksnapshot-v34.4.0-mas-arm64.zip +23bb0f4a35f3aec9fcd6cb904957ad3f719699f18883c89ff5ddf4b5ddfdb350 *mksnapshot-v34.4.0-mas-x64.zip +171dc9123255e37b6f28c7b5ad97c2a70030fef554af50b8cf9e65d3a78faf32 *mksnapshot-v34.4.0-win32-arm64-x64.zip +86e7699c642b0ced11530d1a5bc7c0866063d3350f61eef8afe28b06302cec38 *mksnapshot-v34.4.0-win32-ia32.zip +9d2fe4e95aa56f2b398c774c848ba12194baa32c2ff3c872bb981d99a3fe02ea *mksnapshot-v34.4.0-win32-x64.zip diff --git a/build/gulpfile.scan.js b/build/gulpfile.scan.js index cbcdddb74bc4..aafc64e81c2f 100644 --- a/build/gulpfile.scan.js +++ b/build/gulpfile.scan.js @@ -84,9 +84,8 @@ function nodeModules(destinationExe, destinationPdb, platform) { '**/*.node', // Exclude these paths. // We don't build the prebuilt node files so we don't scan them - '!**/prebuilds/**/*.node', - // These are 3rd party modules that we should ignore - '!**/@parcel/watcher/**/*'])) + '!**/prebuilds/**/*.node' + ])) .pipe(gulp.dest(destinationExe)); }; diff --git a/build/lib/layersChecker.js b/build/lib/layersChecker.js index 9785bb8d3794..ba269af9af59 100644 --- a/build/lib/layersChecker.js +++ b/build/lib/layersChecker.js @@ -85,6 +85,9 @@ const CORE_TYPES = [ 'SubtleCrypto', 'JsonWebKey', 'MessageEvent', + // node web types + 'ReadableStreamDefaultReader', + 'DOMException', ]; // 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 f8aa401a0218..39dc920a806e 100644 --- a/build/lib/layersChecker.ts +++ b/build/lib/layersChecker.ts @@ -84,6 +84,10 @@ const CORE_TYPES = [ 'SubtleCrypto', 'JsonWebKey', 'MessageEvent', + + // node web types + 'ReadableStreamDefaultReader', + 'DOMException', ]; // 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 dbca887bc227..f26c23a83f0c 100644 --- a/build/lib/propertyInitOrderChecker.js +++ b/build/lib/propertyInitOrderChecker.js @@ -54,16 +54,11 @@ const TS_CONFIG_PATH = path.join(__dirname, '../../', 'src', 'tsconfig.json'); // ############################################################################################# // const ignored = new Set([ - 'vs/base/common/arrays.ts', - 'vs/platform/extensionManagement/common/extensionsScannerService.ts', - 'vs/platform/configuration/common/configurations.ts', '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/workbench/contrib/debug/common/debugStorage.ts', - 'vs/workbench/contrib/debug/common/debugModel.ts', 'vs/workbench/api/common/extHostCommands.ts', 'vs/editor/browser/view/viewLayer.ts', 'vs/editor/browser/controller/editContext/textArea/textAreaEditContextInput.ts', @@ -115,11 +110,7 @@ const ignored = new Set([ 'vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel.ts', 'vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput.ts', 'vs/platform/terminal/common/capabilities/commandDetectionCapability.ts', - 'vs/workbench/contrib/testing/common/testExclusions.ts', - 'vs/workbench/contrib/testing/common/testResultStorage.ts', - 'vs/workbench/services/userDataProfile/browser/snippetsResource.ts', 'vs/platform/quickinput/browser/quickInputController.ts', - 'vs/platform/userDataSync/common/abstractSynchronizer.ts', 'vs/workbench/services/authentication/browser/authenticationExtensionsService.ts', 'vs/workbench/services/textMate/browser/backgroundTokenization/textMateWorkerTokenizerController.ts', 'vs/workbench/services/textMate/browser/textMateTokenizationFeatureImpl.ts', @@ -133,37 +124,21 @@ const ignored = new Set([ 'vs/workbench/contrib/search/common/cacheState.ts', 'vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts', 'vs/workbench/contrib/search/browser/anythingQuickAccess.ts', - 'vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts', - 'vs/workbench/contrib/testing/browser/testResultsView/testResultsOutput.ts', - 'vs/workbench/contrib/testing/common/testExplorerFilterState.ts', - 'vs/workbench/contrib/testing/browser/testResultsView/testResultsTree.ts', - 'vs/workbench/contrib/testing/browser/testingOutputPeek.ts', - 'vs/workbench/contrib/testing/browser/explorerProjections/index.ts', - 'vs/workbench/contrib/testing/browser/testingExplorerFilter.ts', - 'vs/workbench/contrib/testing/browser/testingExplorerView.ts', - 'vs/workbench/contrib/testing/common/testServiceImpl.ts', 'vs/platform/quickinput/browser/commandsQuickAccess.ts', 'vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts', 'vs/workbench/contrib/multiDiffEditor/browser/scmMultiDiffSourceResolver.ts', - 'vs/workbench/contrib/debug/browser/debugMemory.ts', - 'vs/workbench/contrib/markers/browser/markersViewActions.ts', 'vs/workbench/contrib/mergeEditor/browser/view/viewZones.ts', 'vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts', - 'vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts', - 'vs/workbench/contrib/output/browser/outputServices.ts', - 'vs/workbench/contrib/terminalContrib/typeAhead/browser/terminalTypeAheadAddon.ts', 'vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts', 'vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts', 'vs/workbench/contrib/accessibilitySignals/browser/editorTextPropertySignalsContribution.ts', 'vs/workbench/contrib/inlineCompletions/browser/inlineCompletionLanguageStatusBarContribution.ts', - 'vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts', 'vs/workbench/contrib/welcomeDialog/browser/welcomeWidget.ts', 'vs/editor/standalone/browser/quickInput/standaloneQuickInputService.ts', 'vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsWordInsertView.ts', 'vs/platform/terminal/node/ptyService.ts', 'vs/workbench/api/common/extHostLanguageFeatures.ts', 'vs/workbench/api/common/extHostSearch.ts', - 'vs/workbench/contrib/testing/test/common/testStubs.ts' ]); const cancellationToken = { isCancellationRequested: () => false, diff --git a/build/lib/propertyInitOrderChecker.ts b/build/lib/propertyInitOrderChecker.ts index dc18213566f5..8e8b259fcc18 100644 --- a/build/lib/propertyInitOrderChecker.ts +++ b/build/lib/propertyInitOrderChecker.ts @@ -23,16 +23,11 @@ const TS_CONFIG_PATH = path.join(__dirname, '../../', 'src', 'tsconfig.json'); // const ignored = new Set([ - 'vs/base/common/arrays.ts', - 'vs/platform/extensionManagement/common/extensionsScannerService.ts', - 'vs/platform/configuration/common/configurations.ts', '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/workbench/contrib/debug/common/debugStorage.ts', - 'vs/workbench/contrib/debug/common/debugModel.ts', 'vs/workbench/api/common/extHostCommands.ts', 'vs/editor/browser/view/viewLayer.ts', 'vs/editor/browser/controller/editContext/textArea/textAreaEditContextInput.ts', @@ -84,11 +79,7 @@ const ignored = new Set([ 'vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel.ts', 'vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput.ts', 'vs/platform/terminal/common/capabilities/commandDetectionCapability.ts', - 'vs/workbench/contrib/testing/common/testExclusions.ts', - 'vs/workbench/contrib/testing/common/testResultStorage.ts', - 'vs/workbench/services/userDataProfile/browser/snippetsResource.ts', 'vs/platform/quickinput/browser/quickInputController.ts', - 'vs/platform/userDataSync/common/abstractSynchronizer.ts', 'vs/workbench/services/authentication/browser/authenticationExtensionsService.ts', 'vs/workbench/services/textMate/browser/backgroundTokenization/textMateWorkerTokenizerController.ts', 'vs/workbench/services/textMate/browser/textMateTokenizationFeatureImpl.ts', @@ -102,37 +93,21 @@ const ignored = new Set([ 'vs/workbench/contrib/search/common/cacheState.ts', 'vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts', 'vs/workbench/contrib/search/browser/anythingQuickAccess.ts', - 'vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts', - 'vs/workbench/contrib/testing/browser/testResultsView/testResultsOutput.ts', - 'vs/workbench/contrib/testing/common/testExplorerFilterState.ts', - 'vs/workbench/contrib/testing/browser/testResultsView/testResultsTree.ts', - 'vs/workbench/contrib/testing/browser/testingOutputPeek.ts', - 'vs/workbench/contrib/testing/browser/explorerProjections/index.ts', - 'vs/workbench/contrib/testing/browser/testingExplorerFilter.ts', - 'vs/workbench/contrib/testing/browser/testingExplorerView.ts', - 'vs/workbench/contrib/testing/common/testServiceImpl.ts', 'vs/platform/quickinput/browser/commandsQuickAccess.ts', 'vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts', 'vs/workbench/contrib/multiDiffEditor/browser/scmMultiDiffSourceResolver.ts', - 'vs/workbench/contrib/debug/browser/debugMemory.ts', - 'vs/workbench/contrib/markers/browser/markersViewActions.ts', 'vs/workbench/contrib/mergeEditor/browser/view/viewZones.ts', 'vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts', - 'vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts', - 'vs/workbench/contrib/output/browser/outputServices.ts', - 'vs/workbench/contrib/terminalContrib/typeAhead/browser/terminalTypeAheadAddon.ts', 'vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts', 'vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts', 'vs/workbench/contrib/accessibilitySignals/browser/editorTextPropertySignalsContribution.ts', 'vs/workbench/contrib/inlineCompletions/browser/inlineCompletionLanguageStatusBarContribution.ts', - 'vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts', 'vs/workbench/contrib/welcomeDialog/browser/welcomeWidget.ts', 'vs/editor/standalone/browser/quickInput/standaloneQuickInputService.ts', 'vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsWordInsertView.ts', 'vs/platform/terminal/node/ptyService.ts', 'vs/workbench/api/common/extHostLanguageFeatures.ts', 'vs/workbench/api/common/extHostSearch.ts', - 'vs/workbench/contrib/testing/test/common/testStubs.ts' ]); diff --git a/build/lib/standalone.js b/build/lib/standalone.js index fb6c4fcf8fee..732a34228b92 100644 --- a/build/lib/standalone.js +++ b/build/lib/standalone.js @@ -141,7 +141,8 @@ function extractEditor(options) { delete tsConfig.compilerOptions.moduleResolution; writeOutputFile('tsconfig.json', JSON.stringify(tsConfig, null, '\t')); [ - 'vs/loader.js' + 'vs/loader.js', + 'typings/css.d.ts' ].forEach(copyFile); } function transportCSS(module, enqueue, write) { diff --git a/build/lib/standalone.ts b/build/lib/standalone.ts index be7d07f76cae..b18908dcb038 100644 --- a/build/lib/standalone.ts +++ b/build/lib/standalone.ts @@ -118,7 +118,8 @@ export function extractEditor(options: tss.ITreeShakingOptions & { destRoot: str writeOutputFile('tsconfig.json', JSON.stringify(tsConfig, null, '\t')); [ - 'vs/loader.js' + 'vs/loader.js', + 'typings/css.d.ts' ].forEach(copyFile); } diff --git a/build/lib/tsb/builder.js b/build/lib/tsb/builder.js index 84308191361a..0da1f5c09e6f 100644 --- a/build/lib/tsb/builder.js +++ b/build/lib/tsb/builder.js @@ -558,10 +558,11 @@ class LanguageServiceHost { return old; } removeScriptSnapshot(filename) { + filename = normalize(filename); + this._log('removeScriptSnapshot', filename); this._filesInProject.delete(filename); this._filesAdded.delete(filename); this._projectVersion++; - filename = normalize(filename); delete this._fileNameToDeclaredModule[filename]; return delete this._snapshots[filename]; } @@ -622,6 +623,9 @@ class LanguageServiceHost { // node module? return; } + if (ref.fileName.endsWith('.css')) { + return; + } const stopDirname = normalize(this.getCurrentDirectory()); let dirname = filename; let found = false; diff --git a/build/lib/tsb/builder.ts b/build/lib/tsb/builder.ts index 7a1b0e0cbb44..1a68131f86d5 100644 --- a/build/lib/tsb/builder.ts +++ b/build/lib/tsb/builder.ts @@ -630,10 +630,11 @@ class LanguageServiceHost implements ts.LanguageServiceHost { } removeScriptSnapshot(filename: string): boolean { + filename = normalize(filename); + this._log('removeScriptSnapshot', filename); this._filesInProject.delete(filename); this._filesAdded.delete(filename); this._projectVersion++; - filename = normalize(filename); delete this._fileNameToDeclaredModule[filename]; return delete this._snapshots[filename]; } @@ -706,7 +707,9 @@ class LanguageServiceHost implements ts.LanguageServiceHost { // node module? return; } - + if (ref.fileName.endsWith('.css')) { + return; + } const stopDirname = normalize(this.getCurrentDirectory()); let dirname = filename; diff --git a/build/package-lock.json b/build/package-lock.json index 445e842c5e32..199fc9de1407 100644 --- a/build/package-lock.json +++ b/build/package-lock.json @@ -1192,12 +1192,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": "20.17.27", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.27.tgz", + "integrity": "sha512-U58sbKhDrthHlxHRJw7ZLiLDZGmAUOZUbpw0S6nL27sYUdhvgBLCRu/keSd6qcTsfArd1sRFCCBxzWATGr/0UA==", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.2" } }, "node_modules/@types/pump": { @@ -4141,10 +4142,11 @@ } }, "node_modules/tar-fs": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", - "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz", + "integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==", "dev": true, + "license": "MIT", "optional": true, "dependencies": { "chownr": "^1.1.1", @@ -4385,10 +4387,11 @@ "dev": true }, "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.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" }, "node_modules/universal-user-agent": { "version": "6.0.0", diff --git a/cgmanifest.json b/cgmanifest.json index eb5b37d39a75..e89279248979 100644 --- a/cgmanifest.json +++ b/cgmanifest.json @@ -528,12 +528,12 @@ "git": { "name": "electron", "repositoryUrl": "https://github.com/electron/electron", - "commitHash": "f98501308a973e0aee2414315b426e5de2c03a60" + "commitHash": "9049d01bd688674dd3d69aa0de992f7130df7f93" } }, "isOnlyProductionDependency": true, "license": "MIT", - "version": "34.3.2" + "version": "34.4.0" }, { "component": { diff --git a/eslint.config.js b/eslint.config.js index 822da54456b5..33165a230871 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -88,6 +88,7 @@ export default tseslint.config( 'local/code-no-unexternalized-strings': 'warn', 'local/code-must-use-super-dispose': 'warn', 'local/code-declare-service-brand': 'warn', + 'local/code-no-deep-import-of-internal': ['error', { '.*Internal': true, 'searchExtTypesInternal': false }], 'local/code-layering': [ 'warn', { diff --git a/extensions/css-language-features/client/src/cssClient.ts b/extensions/css-language-features/client/src/cssClient.ts index f6e8fe3513e1..4e90b3482e44 100644 --- a/extensions/css-language-features/client/src/cssClient.ts +++ b/extensions/css-language-features/client/src/cssClient.ts @@ -15,7 +15,7 @@ namespace CustomDataChangedNotification { export type LanguageClientConstructor = (name: string, description: string, clientOptions: LanguageClientOptions) => BaseLanguageClient; export interface Runtime { - TextDecoder: { new(encoding?: string): { decode(buffer: ArrayBuffer): string } }; + TextDecoder: typeof TextDecoder; fs?: RequestService; } diff --git a/extensions/css-language-features/client/src/node/cssClientMain.ts b/extensions/css-language-features/client/src/node/cssClientMain.ts index 96926979b2a6..f634188bedf7 100644 --- a/extensions/css-language-features/client/src/node/cssClientMain.ts +++ b/extensions/css-language-features/client/src/node/cssClientMain.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { TextDecoder } from 'util'; import { ExtensionContext, extensions, l10n } from 'vscode'; import { BaseLanguageClient, LanguageClient, LanguageClientOptions, ServerOptions, TransportKind } from 'vscode-languageclient/node'; import { LanguageClientConstructor, startClient } from '../cssClient'; diff --git a/extensions/css-language-features/package-lock.json b/extensions/css-language-features/package-lock.json index c843143c01fa..6414bc9a6449 100644 --- a/extensions/css-language-features/package-lock.json +++ b/extensions/css-language-features/package-lock.json @@ -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": "20.17.27", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.27.tgz", + "integrity": "sha512-U58sbKhDrthHlxHRJw7ZLiLDZGmAUOZUbpw0S6nL27sYUdhvgBLCRu/keSd6qcTsfArd1sRFCCBxzWATGr/0UA==", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.2" } }, "node_modules/balanced-match": { @@ -71,10 +72,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.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" }, "node_modules/vscode-jsonrpc": { "version": "9.0.0-next.7", diff --git a/extensions/debug-auto-launch/package-lock.json b/extensions/debug-auto-launch/package-lock.json index 84a1daab83bb..d6a69a857f58 100644 --- a/extensions/debug-auto-launch/package-lock.json +++ b/extensions/debug-auto-launch/package-lock.json @@ -16,19 +16,21 @@ } }, "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": "20.17.27", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.27.tgz", + "integrity": "sha512-U58sbKhDrthHlxHRJw7ZLiLDZGmAUOZUbpw0S6nL27sYUdhvgBLCRu/keSd6qcTsfArd1sRFCCBxzWATGr/0UA==", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.2" } }, "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.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" } } } diff --git a/extensions/emmet/package-lock.json b/extensions/emmet/package-lock.json index cadbf5387a8f..971528b395b7 100644 --- a/extensions/emmet/package-lock.json +++ b/extensions/emmet/package-lock.json @@ -81,12 +81,13 @@ "integrity": "sha1-JEywLHfsLnT3ipvTGCGKvJxQCmE= sha512-ZsZ2I9Vzso3Ho/pjZFsmmZ++FWeEd/txqybHTm4OgaZzdS8V9V/YYWQwg5TC38Z7uLWUV1vavpLLbjJtKubR1A==" }, "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": "20.17.27", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.27.tgz", + "integrity": "sha512-U58sbKhDrthHlxHRJw7ZLiLDZGmAUOZUbpw0S6nL27sYUdhvgBLCRu/keSd6qcTsfArd1sRFCCBxzWATGr/0UA==", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.2" } }, "node_modules/@vscode/emmet-helper": { @@ -151,10 +152,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.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" }, "node_modules/vscode-languageserver-textdocument": { "version": "1.0.12", diff --git a/extensions/git/package-lock.json b/extensions/git/package-lock.json index 2629a9404440..16b71e40c371 100644 --- a/extensions/git/package-lock.json +++ b/extensions/git/package-lock.json @@ -182,12 +182,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": "20.17.27", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.27.tgz", + "integrity": "sha512-U58sbKhDrthHlxHRJw7ZLiLDZGmAUOZUbpw0S6nL27sYUdhvgBLCRu/keSd6qcTsfArd1sRFCCBxzWATGr/0UA==", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.2" } }, "node_modules/@types/picomatch": { @@ -383,10 +384,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.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" }, "node_modules/util-deprecate": { "version": "1.0.2", diff --git a/extensions/git/package.json b/extensions/git/package.json index 6761f02ce5bf..255bb02be3fb 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -3298,13 +3298,13 @@ "markdownDescription": "%config.commitShortHashLength%", "scope": "resource" }, - "git.diagnosticsCommitHook.Enabled": { + "git.diagnosticsCommitHook.enabled": { "type": "boolean", "default": false, - "markdownDescription": "%config.diagnosticsCommitHook.Enabled%", + "markdownDescription": "%config.diagnosticsCommitHook.enabled%", "scope": "resource" }, - "git.diagnosticsCommitHook.Sources": { + "git.diagnosticsCommitHook.sources": { "type": "object", "additionalProperties": { "type": "string", @@ -3319,7 +3319,7 @@ "default": { "*": "error" }, - "markdownDescription": "%config.diagnosticsCommitHook.Sources%", + "markdownDescription": "%config.diagnosticsCommitHook.sources%", "scope": "resource" }, "git.discardUntrackedChangesToTrash": { diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index 52ba38198170..dfc2ce99ca80 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -286,8 +286,8 @@ "config.blameStatusBarItem.enabled": "Controls whether to show blame information in the status bar.", "config.blameStatusBarItem.template": "Template for the blame information status bar item. Supported variables:\n\n* `hash`: Commit hash\n\n* `hashShort`: First N characters of the commit hash according to `#git.commitShortHashLength#`\n\n* `subject`: First line of the commit message\n\n* `authorName`: Author name\n\n* `authorEmail`: Author email\n\n* `authorDate`: Author date\n\n* `authorDateAgo`: Time difference between now and the author date\n\n", "config.commitShortHashLength": "Controls the length of the commit short hash.", - "config.diagnosticsCommitHook.Enabled": "Controls whether to check for unresolved diagnostics before committing.", - "config.diagnosticsCommitHook.Sources": "Controls the list of sources (**Item**) and the minimum severity (**Value**) to be considered before committing. **Note:** To ignore diagnostics from a particular source, add the source to the list and set the minimum severity to `none`.", + "config.diagnosticsCommitHook.enabled": "Controls whether to check for unresolved diagnostics before committing.", + "config.diagnosticsCommitHook.sources": "Controls the list of sources (**Item**) and the minimum severity (**Value**) to be considered before committing. **Note:** To ignore diagnostics from a particular source, add the source to the list and set the minimum severity to `none`.", "config.discardUntrackedChangesToTrash": "Controls whether discarding untracked changes moves the file(s) to the Recycle Bin (Windows), Trash (macOS, Linux) instead of deleting them permanently. **Note:** This setting has no effect when connected to a remote or when running in Linux as a snap package.", "config.showReferenceDetails": "Controls whether to show the details of the last commit for Git refs in the checkout, branch, and tag pickers.", "submenu.explorer": "Git", diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 176a95b5432a..e8e76bddfaa8 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -632,8 +632,8 @@ class CommandErrorOutputTextDocumentContentProvider implements TextDocumentConte async function evaluateDiagnosticsCommitHook(repository: Repository, options: CommitOptions): Promise { const config = workspace.getConfiguration('git', Uri.file(repository.root)); - const enabled = config.get('diagnosticsCommitHook.Enabled', false) === true; - const sourceSeverity = config.get>('diagnosticsCommitHook.Sources', { '*': 'error' }); + const enabled = config.get('diagnosticsCommitHook.enabled', false) === true; + const sourceSeverity = config.get>('diagnosticsCommitHook.sources', { '*': 'error' }); if (!enabled) { return true; @@ -2867,6 +2867,7 @@ export class CommandCenter { const branchWhitespaceChar = config.get('branchWhitespaceChar')!; const branchValidationRegex = config.get('branchValidationRegex')!; const branchRandomNameEnabled = config.get('branchRandomName.enable', false); + const refs = await repository.getRefs({ pattern: 'refs/heads' }); if (defaultName) { return sanitizeBranchName(defaultName, branchWhitespaceChar); @@ -2884,6 +2885,13 @@ export class CommandCenter { const getValidationMessage = (name: string): string | InputBoxValidationMessage | undefined => { const validateName = new RegExp(branchValidationRegex); const sanitizedName = sanitizeBranchName(name, branchWhitespaceChar); + + // Check if branch name already exists + const existingBranch = refs.find(ref => ref.name === sanitizedName); + if (existingBranch) { + return l10n.t('Branch "{0}" already exists', sanitizedName); + } + if (validateName.test(sanitizedName)) { // If the sanitized name that we will use is different than what is // in the input box, show an info message to the user informing them diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index e42ca1e00d88..3433770b1f95 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -11,7 +11,7 @@ import { fileURLToPath } from 'url'; import which from 'which'; import { EventEmitter } from 'events'; import * as filetype from 'file-type'; -import { assign, groupBy, IDisposable, toDisposable, dispose, mkdirp, readBytes, detectUnicodeEncoding, Encoding, onceEvent, splitInChunks, Limiter, Versions, isWindows, pathEquals, isMacintosh, isDescendant, relativePath } from './util'; +import { assign, groupBy, IDisposable, toDisposable, dispose, mkdirp, readBytes, detectUnicodeEncoding, Encoding, onceEvent, splitInChunks, Limiter, Versions, isWindows, pathEquals, isMacintosh, isDescendant, relativePathWithNoFallback } from './util'; import { CancellationError, CancellationToken, ConfigurationChangeEvent, LogOutputChannel, Progress, Uri, workspace } from 'vscode'; import { Commit as ApiCommit, Ref, RefType, Branch, Remote, ForcePushMode, GitErrorCodes, LogOptions, Change, Status, CommitOptions, RefQuery as ApiRefQuery, InitOptions } from './api/git'; import * as byline from 'byline'; @@ -352,8 +352,8 @@ function sanitizePath(path: string): string { return path.replace(/^([a-z]):\\/i, (_, letter) => `${letter.toUpperCase()}:\\`); } -function sanitizeRelativePath(from: string, to: string): string { - return path.isAbsolute(to) ? relativePath(from, to).replace(/\\/g, '/') : to; +function sanitizeRelativePath(path: string): string { + return path.replace(/\\/g, '/'); } const COMMIT_FORMAT = '%H%n%aN%n%aE%n%at%n%ct%n%P%n%D%n%B'; @@ -1402,7 +1402,7 @@ export class Repository { } async buffer(ref: string, filePath: string): Promise { - const relativePath = sanitizeRelativePath(this.repositoryRoot, filePath); + const relativePath = this.sanitizeRelativePath(filePath); const child = this.stream(['show', '--textconv', `${ref}:${relativePath}`]); if (!child.stdout) { @@ -1465,7 +1465,7 @@ export class Repository { args.push(treeish); if (path) { - args.push('--', sanitizeRelativePath(this.repositoryRoot, path)); + args.push('--', this.sanitizeRelativePath(path)); } const { stdout } = await this.exec(args); @@ -1474,7 +1474,7 @@ export class Repository { async lsfiles(path: string): Promise { const args = ['ls-files', '--stage']; - const relativePath = sanitizeRelativePath(this.repositoryRoot, path); + const relativePath = this.sanitizeRelativePath(path); if (relativePath) { args.push('--', relativePath); @@ -1489,7 +1489,7 @@ export class Repository { ? await this.lstree(ref, undefined, { recursive: true }) : await this.lsfiles(this.repositoryRoot); - const relativePathLowercase = sanitizeRelativePath(this.repositoryRoot, filePath).toLowerCase(); + const relativePathLowercase = this.sanitizeRelativePath(filePath).toLowerCase(); const element = elements.find(file => file.file.toLowerCase() === relativePathLowercase); if (!element) { @@ -1578,7 +1578,7 @@ export class Repository { return await this.diffFiles(false); } - const args = ['diff', '--', sanitizeRelativePath(this.repositoryRoot, path)]; + const args = ['diff', '--', this.sanitizeRelativePath(path)]; const result = await this.exec(args); return result.stdout; } @@ -1591,7 +1591,7 @@ export class Repository { return await this.diffFiles(false, ref); } - const args = ['diff', ref, '--', sanitizeRelativePath(this.repositoryRoot, path)]; + const args = ['diff', ref, '--', this.sanitizeRelativePath(path)]; const result = await this.exec(args); return result.stdout; } @@ -1604,7 +1604,7 @@ export class Repository { return await this.diffFiles(true); } - const args = ['diff', '--cached', '--', sanitizeRelativePath(this.repositoryRoot, path)]; + const args = ['diff', '--cached', '--', this.sanitizeRelativePath(path)]; const result = await this.exec(args); return result.stdout; } @@ -1617,7 +1617,7 @@ export class Repository { return await this.diffFiles(true, ref); } - const args = ['diff', '--cached', ref, '--', sanitizeRelativePath(this.repositoryRoot, path)]; + const args = ['diff', '--cached', ref, '--', this.sanitizeRelativePath(path)]; const result = await this.exec(args); return result.stdout; } @@ -1637,7 +1637,7 @@ export class Repository { return await this.diffFiles(false, range); } - const args = ['diff', range, '--', sanitizeRelativePath(this.repositoryRoot, path)]; + const args = ['diff', range, '--', this.sanitizeRelativePath(path)]; const result = await this.exec(args); return result.stdout.trim(); @@ -1729,7 +1729,7 @@ export class Repository { } if (paths && paths.length) { - for (const chunk of splitInChunks(paths.map(p => sanitizeRelativePath(this.repositoryRoot, p)), MAX_CLI_LENGTH)) { + for (const chunk of splitInChunks(paths.map(p => this.sanitizeRelativePath(p)), MAX_CLI_LENGTH)) { await this.exec([...args, '--', ...chunk]); } } else { @@ -1744,13 +1744,13 @@ export class Repository { return; } - args.push(...paths.map(p => sanitizeRelativePath(this.repositoryRoot, p))); + args.push(...paths.map(p => this.sanitizeRelativePath(p))); await this.exec(args); } async stage(path: string, data: Uint8Array): Promise { - const relativePath = sanitizeRelativePath(this.repositoryRoot, path); + const relativePath = this.sanitizeRelativePath(path); const child = this.stream(['hash-object', '--stdin', '-w', '--path', relativePath], { stdio: [null, null, null] }); child.stdin!.end(data); @@ -1800,7 +1800,7 @@ export class Repository { try { if (paths && paths.length > 0) { - for (const chunk of splitInChunks(paths.map(p => sanitizeRelativePath(this.repositoryRoot, p)), MAX_CLI_LENGTH)) { + for (const chunk of splitInChunks(paths.map(p => this.sanitizeRelativePath(p)), MAX_CLI_LENGTH)) { await this.exec([...args, '--', ...chunk]); } } else { @@ -2014,7 +2014,7 @@ export class Repository { const args = ['clean', '-f', '-q']; for (const paths of groups) { - for (const chunk of splitInChunks(paths.map(p => sanitizeRelativePath(this.repositoryRoot, p)), MAX_CLI_LENGTH)) { + for (const chunk of splitInChunks(paths.map(p => this.sanitizeRelativePath(p)), MAX_CLI_LENGTH)) { promises.push(limiter.queue(() => this.exec([...args, '--', ...chunk]))); } } @@ -2054,7 +2054,7 @@ export class Repository { try { if (paths && paths.length > 0) { - for (const chunk of splitInChunks(paths.map(p => sanitizeRelativePath(this.repositoryRoot, p)), MAX_CLI_LENGTH)) { + for (const chunk of splitInChunks(paths.map(p => this.sanitizeRelativePath(p)), MAX_CLI_LENGTH)) { await this.exec([...args, '--', ...chunk]); } } else { @@ -2299,7 +2299,7 @@ export class Repository { async blame(path: string): Promise { try { - const args = ['blame', '--', sanitizeRelativePath(this.repositoryRoot, path)]; + const args = ['blame', '--', this.sanitizeRelativePath(path)]; const result = await this.exec(args); return result.stdout.trim(); } catch (err) { @@ -2319,7 +2319,7 @@ export class Repository { args.push(ref); } - args.push('--', sanitizeRelativePath(this.repositoryRoot, path)); + args.push('--', this.sanitizeRelativePath(path)); const result = await this.exec(args); @@ -2958,7 +2958,7 @@ export class Repository { async updateSubmodules(paths: string[]): Promise { const args = ['submodule', 'update']; - for (const chunk of splitInChunks(paths.map(p => sanitizeRelativePath(this.repositoryRoot, p)), MAX_CLI_LENGTH)) { + for (const chunk of splitInChunks(paths.map(p => this.sanitizeRelativePath(p)), MAX_CLI_LENGTH)) { await this.exec([...args, '--', ...chunk]); } } @@ -2977,4 +2977,40 @@ export class Repository { throw err; } } + + private sanitizeRelativePath(filePath: string): string { + this.logger.trace(`[Git][sanitizeRelativePath] filePath: ${filePath}`); + + // Relative path + if (!path.isAbsolute(filePath)) { + filePath = sanitizeRelativePath(filePath); + this.logger.trace(`[Git][sanitizeRelativePath] relativePath (noop): ${filePath}`); + return filePath; + } + + let relativePath: string | undefined; + + // Repository root real path + if (this.repositoryRootRealPath) { + relativePath = relativePathWithNoFallback(this.repositoryRootRealPath, filePath); + if (relativePath) { + relativePath = sanitizeRelativePath(relativePath); + this.logger.trace(`[Git][sanitizeRelativePath] relativePath (real path): ${relativePath}`); + return relativePath; + } + } + + // Repository root path + relativePath = relativePathWithNoFallback(this.repositoryRoot, filePath); + if (relativePath) { + relativePath = sanitizeRelativePath(relativePath); + this.logger.trace(`[Git][sanitizeRelativePath] relativePath (path): ${relativePath}`); + return relativePath; + } + + // Fallback to the original path + filePath = sanitizeRelativePath(filePath); + this.logger.trace(`[Git][sanitizeRelativePath] relativePath (fallback): ${filePath}`); + return filePath; + } } diff --git a/extensions/git/src/util.ts b/extensions/git/src/util.ts index 7dd1cbafbdf0..1ef59205883d 100644 --- a/extensions/git/src/util.ts +++ b/extensions/git/src/util.ts @@ -328,6 +328,10 @@ export function pathEquals(a: string, b: string): boolean { * casing which is why we attempt to use substring() before relative(). */ export function relativePath(from: string, to: string): string { + return relativePathWithNoFallback(from, to) ?? relative(from, to); +} + +export function relativePathWithNoFallback(from: string, to: string): string | undefined { // There are cases in which the `from` path may contain a trailing separator at // the end (ex: "C:\", "\\server\folder\" (Windows) or "/" (Linux/macOS)) which // is by design as documented in https://github.com/nodejs/node/issues/1765. If @@ -340,8 +344,7 @@ export function relativePath(from: string, to: string): string { return to.substring(from.length); } - // Fallback to `path.relative` - return relative(from, to); + return undefined; } export function* splitInChunks(array: string[], maxChunkLength: number): IterableIterator { diff --git a/extensions/html-language-features/client/src/htmlClient.ts b/extensions/html-language-features/client/src/htmlClient.ts index 7b69c795f904..6e66f8da2887 100644 --- a/extensions/html-language-features/client/src/htmlClient.ts +++ b/extensions/html-language-features/client/src/htmlClient.ts @@ -76,7 +76,7 @@ export type LanguageClientConstructor = (name: string, description: string, clie export const languageServerDescription = l10n.t('HTML Language Server'); export interface Runtime { - TextDecoder: { new(encoding?: string): { decode(buffer: ArrayBuffer): string } }; + TextDecoder: typeof TextDecoder; fileFs?: FileSystemProvider; telemetry?: TelemetryReporter; readonly timer: { diff --git a/extensions/html-language-features/client/src/node/htmlClientMain.ts b/extensions/html-language-features/client/src/node/htmlClientMain.ts index cdb995b3286d..1ee0545bc15a 100644 --- a/extensions/html-language-features/client/src/node/htmlClientMain.ts +++ b/extensions/html-language-features/client/src/node/htmlClientMain.ts @@ -7,7 +7,6 @@ import { getNodeFileFS } from './nodeFs'; import { Disposable, ExtensionContext, l10n } from 'vscode'; import { startClient, LanguageClientConstructor, AsyncDisposable } from '../htmlClient'; import { ServerOptions, TransportKind, LanguageClientOptions, LanguageClient } from 'vscode-languageclient/node'; -import { TextDecoder } from 'util'; import * as fs from 'fs'; import TelemetryReporter from '@vscode/extension-telemetry'; diff --git a/extensions/html-language-features/package-lock.json b/extensions/html-language-features/package-lock.json index 562d6751ff5f..8eaf9d81c647 100644 --- a/extensions/html-language-features/package-lock.json +++ b/extensions/html-language-features/package-lock.json @@ -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": "20.17.27", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.27.tgz", + "integrity": "sha512-U58sbKhDrthHlxHRJw7ZLiLDZGmAUOZUbpw0S6nL27sYUdhvgBLCRu/keSd6qcTsfArd1sRFCCBxzWATGr/0UA==", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.2" } }, "node_modules/@vscode/extension-telemetry": { @@ -210,10 +211,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.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" }, "node_modules/vscode-jsonrpc": { "version": "9.0.0-next.7", diff --git a/extensions/media-preview/package.json b/extensions/media-preview/package.json index 7e2b70293fc3..02b0134e4cf8 100644 --- a/extensions/media-preview/package.json +++ b/extensions/media-preview/package.json @@ -90,6 +90,18 @@ "command": "imagePreview.copyImage", "title": "%command.copyImage%", "category": "Image Preview" + }, + { + "command": "imagePreview.reopenAsPreview", + "title": "%command.reopenAsPreview%", + "category": "Image Preview", + "icon": "$(preview)" + }, + { + "command": "imagePreview.reopenAsText", + "title": "%command.reopenAsText%", + "category": "Image Preview", + "icon": "$(go-to-file)" } ], "menus": { @@ -107,6 +119,16 @@ { "command": "imagePreview.copyImage", "when": "false" + }, + { + "command": "imagePreview.reopenAsPreview", + "when": "activeEditor == workbench.editors.files.textFileEditor && resourceExtname == '.svg'", + "group": "navigation" + }, + { + "command": "imagePreview.reopenAsText", + "when": "activeCustomEditorId == 'imagePreview.previewEditor' && resourceExtname == '.svg'", + "group": "navigation" } ], "webview/context": [ @@ -114,6 +136,18 @@ "command": "imagePreview.copyImage", "when": "webviewId == 'imagePreview.previewEditor'" } + ], + "editor/title": [ + { + "command": "imagePreview.reopenAsPreview", + "when": "editorFocus && resourceExtname == '.svg'", + "group": "navigation" + }, + { + "command": "imagePreview.reopenAsText", + "when": "activeCustomEditorId == 'imagePreview.previewEditor' && resourceExtname == '.svg'", + "group": "navigation" + } ] } }, diff --git a/extensions/media-preview/package.nls.json b/extensions/media-preview/package.nls.json index c45e1e2613b3..920ced764352 100644 --- a/extensions/media-preview/package.nls.json +++ b/extensions/media-preview/package.nls.json @@ -8,5 +8,7 @@ "videoPreviewerLoop": "Loop videos over again automatically.", "command.zoomIn": "Zoom in", "command.zoomOut": "Zoom out", - "command.copyImage": "Copy" + "command.copyImage": "Copy", + "command.reopenAsPreview": "Reopen as image preview", + "command.reopenAsText": "Reopen as source text" } diff --git a/extensions/media-preview/src/audioPreview.ts b/extensions/media-preview/src/audioPreview.ts index e21a4189d7ba..5058f7e978e8 100644 --- a/extensions/media-preview/src/audioPreview.ts +++ b/extensions/media-preview/src/audioPreview.ts @@ -54,12 +54,12 @@ class AudioPreview extends MediaPreview { protected async getWebviewContents(): Promise { const version = Date.now().toString(); const settings = { - src: await this.getResourcePath(this.webviewEditor, this.resource, version), + src: await this.getResourcePath(this._webviewEditor, this._resource, version), }; const nonce = getNonce(); - const cspSource = this.webviewEditor.webview.cspSource; + const cspSource = this._webviewEditor.webview.cspSource; return /* html */` @@ -104,7 +104,7 @@ class AudioPreview extends MediaPreview { } private extensionResource(...parts: string[]) { - return this.webviewEditor.webview.asWebviewUri(vscode.Uri.joinPath(this.extensionRoot, ...parts)); + return this._webviewEditor.webview.asWebviewUri(vscode.Uri.joinPath(this.extensionRoot, ...parts)); } } diff --git a/extensions/media-preview/src/imagePreview/index.ts b/extensions/media-preview/src/imagePreview/index.ts index e0c605c2a6e2..b405cd652c48 100644 --- a/extensions/media-preview/src/imagePreview/index.ts +++ b/extensions/media-preview/src/imagePreview/index.ts @@ -11,7 +11,7 @@ import { SizeStatusBarEntry } from './sizeStatusBarEntry'; import { Scale, ZoomStatusBarEntry } from './zoomStatusBarEntry'; -export class PreviewManager implements vscode.CustomReadonlyEditorProvider { +export class ImagePreviewManager implements vscode.CustomReadonlyEditorProvider { public static readonly viewType = 'imagePreview.previewEditor'; @@ -48,7 +48,20 @@ export class PreviewManager implements vscode.CustomReadonlyEditorProvider { }); } - public get activePreview() { return this._activePreview; } + public get activePreview() { + return this._activePreview; + } + + public getPreviewFor(resource: vscode.Uri, viewColumn?: vscode.ViewColumn): ImagePreview | undefined { + for (const preview of this._previews) { + if (preview.resource.toString() === resource.toString()) { + if (!viewColumn || preview.viewColumn === viewColumn) { + return preview; + } + } + } + return undefined; + } private setActivePreview(value: ImagePreview | undefined): void { this._activePreview = value; @@ -94,12 +107,12 @@ class ImagePreview extends MediaPreview { this._register(zoomStatusBarEntry.onDidChangeScale(e => { if (this.previewState === PreviewState.Active) { - this.webviewEditor.webview.postMessage({ type: 'setScale', scale: e.scale }); + this._webviewEditor.webview.postMessage({ type: 'setScale', scale: e.scale }); } })); this._register(webviewEditor.onDidChangeViewState(() => { - this.webviewEditor.webview.postMessage({ type: 'setActive', value: this.webviewEditor.active }); + this._webviewEditor.webview.postMessage({ type: 'setActive', value: this._webviewEditor.active }); })); this._register(webviewEditor.onDidDispose(() => { @@ -121,22 +134,26 @@ class ImagePreview extends MediaPreview { this.zoomStatusBarEntry.hide(this); } + public get viewColumn() { + return this._webviewEditor.viewColumn; + } + public zoomIn() { if (this.previewState === PreviewState.Active) { - this.webviewEditor.webview.postMessage({ type: 'zoomIn' }); + this._webviewEditor.webview.postMessage({ type: 'zoomIn' }); } } public zoomOut() { if (this.previewState === PreviewState.Active) { - this.webviewEditor.webview.postMessage({ type: 'zoomOut' }); + this._webviewEditor.webview.postMessage({ type: 'zoomOut' }); } } public copyImage() { if (this.previewState === PreviewState.Active) { - this.webviewEditor.reveal(); - this.webviewEditor.webview.postMessage({ type: 'copyImage' }); + this._webviewEditor.reveal(); + this._webviewEditor.webview.postMessage({ type: 'copyImage' }); } } @@ -147,7 +164,7 @@ class ImagePreview extends MediaPreview { return; } - if (this.webviewEditor.active) { + if (this._webviewEditor.active) { this.sizeStatusBarEntry.show(this, this._imageSize || ''); this.zoomStatusBarEntry.show(this, this._imageZoom || 'fit'); } else { @@ -155,20 +172,21 @@ class ImagePreview extends MediaPreview { this.zoomStatusBarEntry.hide(this); } } + protected override async render(): Promise { await super.render(); - this.webviewEditor.webview.postMessage({ type: 'setActive', value: this.webviewEditor.active }); + this._webviewEditor.webview.postMessage({ type: 'setActive', value: this._webviewEditor.active }); } protected override async getWebviewContents(): Promise { const version = Date.now().toString(); const settings = { - src: await this.getResourcePath(this.webviewEditor, this.resource, version), + src: await this.getResourcePath(this._webviewEditor, this._resource, version), }; const nonce = getNonce(); - const cspSource = this.webviewEditor.webview.cspSource; + const cspSource = this._webviewEditor.webview.cspSource; return /* html */` @@ -212,7 +230,12 @@ class ImagePreview extends MediaPreview { } private extensionResource(...parts: string[]) { - return this.webviewEditor.webview.asWebviewUri(vscode.Uri.joinPath(this.extensionRoot, ...parts)); + return this._webviewEditor.webview.asWebviewUri(vscode.Uri.joinPath(this.extensionRoot, ...parts)); + } + + public async reopenAsText() { + await vscode.commands.executeCommand('reopenActiveEditorWith', 'default'); + this._webviewEditor.dispose(); } } @@ -226,9 +249,9 @@ export function registerImagePreviewSupport(context: vscode.ExtensionContext, bi const zoomStatusBarEntry = new ZoomStatusBarEntry(); disposables.push(zoomStatusBarEntry); - const previewManager = new PreviewManager(context.extensionUri, sizeStatusBarEntry, binarySizeStatusBarEntry, zoomStatusBarEntry); + const previewManager = new ImagePreviewManager(context.extensionUri, sizeStatusBarEntry, binarySizeStatusBarEntry, zoomStatusBarEntry); - disposables.push(vscode.window.registerCustomEditorProvider(PreviewManager.viewType, previewManager, { + disposables.push(vscode.window.registerCustomEditorProvider(ImagePreviewManager.viewType, previewManager, { supportsMultipleEditorsPerDocument: true, })); @@ -244,5 +267,14 @@ export function registerImagePreviewSupport(context: vscode.ExtensionContext, bi previewManager.activePreview?.copyImage(); })); + disposables.push(vscode.commands.registerCommand('imagePreview.reopenAsText', async () => { + return previewManager.activePreview?.reopenAsText(); + })); + + disposables.push(vscode.commands.registerCommand('imagePreview.reopenAsPreview', async () => { + + await vscode.commands.executeCommand('reopenActiveEditorWith', ImagePreviewManager.viewType); + })); + return vscode.Disposable.from(...disposables); } diff --git a/extensions/media-preview/src/mediaPreview.ts b/extensions/media-preview/src/mediaPreview.ts index 26d1e25dbaae..ccf83166e297 100644 --- a/extensions/media-preview/src/mediaPreview.ts +++ b/extensions/media-preview/src/mediaPreview.ts @@ -8,8 +8,8 @@ import { Utils } from 'vscode-uri'; import { BinarySizeStatusBarEntry } from './binarySizeStatusBarEntry'; import { Disposable } from './util/dispose'; -export function reopenAsText(resource: vscode.Uri, viewColumn: vscode.ViewColumn | undefined) { - vscode.commands.executeCommand('vscode.openWith', resource, 'default', viewColumn); +export async function reopenAsText(resource: vscode.Uri, viewColumn: vscode.ViewColumn | undefined): Promise { + await vscode.commands.executeCommand('vscode.openWith', resource, 'default', viewColumn); } export const enum PreviewState { @@ -25,52 +25,56 @@ export abstract class MediaPreview extends Disposable { constructor( extensionRoot: vscode.Uri, - protected readonly resource: vscode.Uri, - protected readonly webviewEditor: vscode.WebviewPanel, - private readonly binarySizeStatusBarEntry: BinarySizeStatusBarEntry, + protected readonly _resource: vscode.Uri, + protected readonly _webviewEditor: vscode.WebviewPanel, + private readonly _binarySizeStatusBarEntry: BinarySizeStatusBarEntry, ) { super(); - webviewEditor.webview.options = { + _webviewEditor.webview.options = { enableScripts: true, enableForms: false, localResourceRoots: [ - Utils.dirname(resource), + Utils.dirname(_resource), extensionRoot, ] }; - this._register(webviewEditor.onDidChangeViewState(() => { + this._register(_webviewEditor.onDidChangeViewState(() => { this.updateState(); })); - this._register(webviewEditor.onDidDispose(() => { + this._register(_webviewEditor.onDidDispose(() => { this.previewState = PreviewState.Disposed; this.dispose(); })); - const watcher = this._register(vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(resource, '*'))); + const watcher = this._register(vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(_resource, '*'))); this._register(watcher.onDidChange(e => { - if (e.toString() === this.resource.toString()) { + if (e.toString() === this._resource.toString()) { this.updateBinarySize(); this.render(); } })); this._register(watcher.onDidDelete(e => { - if (e.toString() === this.resource.toString()) { - this.webviewEditor.dispose(); + if (e.toString() === this._resource.toString()) { + this._webviewEditor.dispose(); } })); } public override dispose() { super.dispose(); - this.binarySizeStatusBarEntry.hide(this); + this._binarySizeStatusBarEntry.hide(this); + } + + public get resource() { + return this._resource; } protected updateBinarySize() { - vscode.workspace.fs.stat(this.resource).then(({ size }) => { + vscode.workspace.fs.stat(this._resource).then(({ size }) => { this._binarySize = size; this.updateState(); }); @@ -86,7 +90,7 @@ export abstract class MediaPreview extends Disposable { return; } - this.webviewEditor.webview.html = content; + this._webviewEditor.webview.html = content; } protected abstract getWebviewContents(): Promise; @@ -96,11 +100,11 @@ export abstract class MediaPreview extends Disposable { return; } - if (this.webviewEditor.active) { + if (this._webviewEditor.active) { this.previewState = PreviewState.Active; - this.binarySizeStatusBarEntry.show(this, this._binarySize); + this._binarySizeStatusBarEntry.show(this, this._binarySize); } else { - this.binarySizeStatusBarEntry.hide(this); + this._binarySizeStatusBarEntry.hide(this); this.previewState = PreviewState.Visible; } } diff --git a/extensions/media-preview/src/videoPreview.ts b/extensions/media-preview/src/videoPreview.ts index efc6be76a4fc..67012128cf7f 100644 --- a/extensions/media-preview/src/videoPreview.ts +++ b/extensions/media-preview/src/videoPreview.ts @@ -56,14 +56,14 @@ class VideoPreview extends MediaPreview { const version = Date.now().toString(); const configurations = vscode.workspace.getConfiguration('mediaPreview.video'); const settings = { - src: await this.getResourcePath(this.webviewEditor, this.resource, version), + src: await this.getResourcePath(this._webviewEditor, this._resource, version), autoplay: configurations.get('autoPlay'), loop: configurations.get('loop'), }; const nonce = getNonce(); - const cspSource = this.webviewEditor.webview.cspSource; + const cspSource = this._webviewEditor.webview.cspSource; return /* html */` @@ -108,7 +108,7 @@ class VideoPreview extends MediaPreview { } private extensionResource(...parts: string[]) { - return this.webviewEditor.webview.asWebviewUri(vscode.Uri.joinPath(this.extensionRoot, ...parts)); + return this._webviewEditor.webview.asWebviewUri(vscode.Uri.joinPath(this.extensionRoot, ...parts)); } } diff --git a/extensions/merge-conflict/package-lock.json b/extensions/merge-conflict/package-lock.json index 5ee68d290f01..94caad8f57e5 100644 --- a/extensions/merge-conflict/package-lock.json +++ b/extensions/merge-conflict/package-lock.json @@ -143,12 +143,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": "20.17.27", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.27.tgz", + "integrity": "sha512-U58sbKhDrthHlxHRJw7ZLiLDZGmAUOZUbpw0S6nL27sYUdhvgBLCRu/keSd6qcTsfArd1sRFCCBxzWATGr/0UA==", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.2" } }, "node_modules/@vscode/extension-telemetry": { @@ -166,10 +167,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.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" } } } diff --git a/extensions/microsoft-authentication/package-lock.json b/extensions/microsoft-authentication/package-lock.json index 5024815f4caa..1e06208429f5 100644 --- a/extensions/microsoft-authentication/package-lock.json +++ b/extensions/microsoft-authentication/package-lock.json @@ -202,12 +202,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": "20.17.27", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.27.tgz", + "integrity": "sha512-U58sbKhDrthHlxHRJw7ZLiLDZGmAUOZUbpw0S6nL27sYUdhvgBLCRu/keSd6qcTsfArd1sRFCCBxzWATGr/0UA==", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.2" } }, "node_modules/@types/node-fetch": { @@ -456,10 +457,11 @@ "integrity": "sha512-V+uqV66BOQnWxvI6HjDnE4VkInmYZUQ4dgB7gzaDyFyFSK1i1nF/j7DpS9UbQAgV9NaF1XpcyuavnM1qOeiEIg==" }, "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.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" }, "node_modules/uuid": { "version": "8.3.2", diff --git a/extensions/microsoft-authentication/src/AADHelper.ts b/extensions/microsoft-authentication/src/AADHelper.ts index 9722145dd037..ad8dabe75335 100644 --- a/extensions/microsoft-authentication/src/AADHelper.ts +++ b/extensions/microsoft-authentication/src/AADHelper.ts @@ -551,7 +551,7 @@ export class AzureActiveDirectoryService { throw e; } - const id = `${claims.tid}/${(claims.oid ?? (claims.altsecid ?? '' + claims.ipd ?? ''))}`; + const id = `${claims.tid}/${(claims.oid ?? (claims.altsecid ?? '' + claims.ipd))}`; const sessionId = existingId || `${id}/${randomUUID()}`; this._logger.trace(`[${scopeData.scopeStr}] '${sessionId}' Token response parsed successfully.`); return { diff --git a/extensions/terminal-suggest/.vscodeignore b/extensions/terminal-suggest/.vscodeignore index 21df0d1f7ac9..d9b5dc0447cc 100644 --- a/extensions/terminal-suggest/.vscodeignore +++ b/extensions/terminal-suggest/.vscodeignore @@ -8,3 +8,4 @@ package-lock.json fixtures/** scripts/** testWorkspace/** +cgmanifest.json diff --git a/extensions/terminal-suggest/src/completions/code-tunnel-insiders.ts b/extensions/terminal-suggest/src/completions/code-tunnel-insiders.ts index dd54a2439ece..c0a3abc90006 100644 --- a/extensions/terminal-suggest/src/completions/code-tunnel-insiders.ts +++ b/extensions/terminal-suggest/src/completions/code-tunnel-insiders.ts @@ -3,12 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { commonOptions, extensionManagementOptions, troubleshootingOptions, globalTunnelOptions, codeTunnelSubcommands, extTunnelSubcommand, codeTunnelOptions } from './code'; -import codeTunnelCompletionSpec, { codeTunnelSpecOptions } from './code-tunnel'; +import codeTunnelCompletionSpec from './code-tunnel'; const codeTunnelInsidersCompletionSpec: Fig.Spec = { ...codeTunnelCompletionSpec, name: 'code-tunnel-insiders', - description: 'Create a tunnel that\'s accessible on vscode.dev from anywhere, with insider features.', + description: 'Visual Studio Code Insiders', subcommands: [...codeTunnelSubcommands, extTunnelSubcommand], options: [ ...commonOptions, @@ -16,7 +16,6 @@ const codeTunnelInsidersCompletionSpec: Fig.Spec = { ...troubleshootingOptions('code-tunnel-insiders'), ...globalTunnelOptions, ...codeTunnelOptions, - ...codeTunnelSpecOptions ] }; diff --git a/extensions/terminal-suggest/src/completions/code-tunnel.ts b/extensions/terminal-suggest/src/completions/code-tunnel.ts index a53edd52ef2b..6abeabb0db79 100644 --- a/extensions/terminal-suggest/src/completions/code-tunnel.ts +++ b/extensions/terminal-suggest/src/completions/code-tunnel.ts @@ -77,7 +77,6 @@ export const codeTunnelSpecOptions: Fig.Option[] = [ const codeTunnelCompletionSpec: Fig.Spec = { ...code, name: 'code-tunnel', - description: 'Create a tunnel that\'s accessible on vscode.dev from anywhere.', subcommands: [ ...codeTunnelSubcommands, extTunnelSubcommand @@ -87,7 +86,7 @@ const codeTunnelCompletionSpec: Fig.Spec = { ...extensionManagementOptions('code-tunnel'), ...troubleshootingOptions('code-tunnel'), ...globalTunnelOptions, - ...codeTunnelOptions + ...codeTunnelOptions, ] }; diff --git a/extensions/terminal-suggest/src/completions/code.ts b/extensions/terminal-suggest/src/completions/code.ts index 1ae5957ac612..a6dfd47738e8 100644 --- a/extensions/terminal-suggest/src/completions/code.ts +++ b/extensions/terminal-suggest/src/completions/code.ts @@ -546,11 +546,6 @@ export const codeTunnelSubcommands = [ name: 'name', }, }, - { - name: 'status', - description: 'Print process usage and diagnostics information', - options: [...globalTunnelOptions, ...tunnelHelpOptions], - }, { name: 'unregister', description: 'Remove this machine\'s association with the port forwarding service', @@ -769,11 +764,12 @@ export const codeTunnelSubcommands = [ { name: 'status', description: 'Print process usage and diagnostics information', + options: [...globalTunnelOptions, ...tunnelHelpOptions], }, { name: 'version', description: `Changes the version of the editor you're using`, - options: globalTunnelOptions + options: [...globalTunnelOptions, ...tunnelHelpOptions], }, { name: 'serve-web', diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts index 8ca8f977144c..18efa329a0f2 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -275,16 +275,15 @@ export async function getCompletionItemsFromSpecs( vscode.TerminalCompletionItemKind.Method )); labels.add(commandTextLabel); - } else { + } + else { const existingItem = items.find(i => (typeof i.label === 'string' ? i.label : i.label.label) === commandTextLabel); if (!existingItem) { continue; } - const preferredItem = compareItems(existingItem, command); - if (preferredItem) { - preferredItem.kind = vscode.TerminalCompletionItemKind.Method; - items.splice(items.indexOf(existingItem), 1, preferredItem); - } + + existingItem.documentation ??= command.documentation; + existingItem.detail ??= command.detail; } } filesRequested = true; @@ -306,20 +305,6 @@ export async function getCompletionItemsFromSpecs( return { items, filesRequested, foldersRequested, fileExtensions, cwd }; } -function compareItems(existingItem: vscode.TerminalCompletionItem, command: ICompletionResource): vscode.TerminalCompletionItem | undefined { - let score = typeof command.label === 'object' ? (command.label.detail !== undefined ? 1 : 0) : 0; - score += typeof command.label === 'object' ? (command.label.description !== undefined ? 2 : 0) : 0; - score += command.documentation ? typeof command.documentation === 'string' ? 2 : 3 : 0; - if (score > 0) { - score -= typeof existingItem.label === 'object' ? (existingItem.label.detail !== undefined ? 1 : 0) : 0; - score -= typeof existingItem.label === 'object' ? (existingItem.label.description !== undefined ? 2 : 0) : 0; - score -= existingItem.documentation ? typeof existingItem.documentation === 'string' ? 2 : 3 : 0; - if (score >= 0) { - return { ...command, replacementIndex: existingItem.replacementIndex, replacementLength: existingItem.replacementLength }; - } - } -} - function getEnvAsRecord(shellIntegrationEnv: ITerminalEnvironment): Record { const env: Record = {}; for (const [key, value] of Object.entries(shellIntegrationEnv ?? process.env)) { diff --git a/extensions/terminal-suggest/src/test/completions/code-insiders.test.ts b/extensions/terminal-suggest/src/test/completions/code-insiders.test.ts index 447772ad8b7f..f64688066e3a 100644 --- a/extensions/terminal-suggest/src/test/completions/code-insiders.test.ts +++ b/extensions/terminal-suggest/src/test/completions/code-insiders.test.ts @@ -4,8 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import codeInsidersCompletionSpec from '../../completions/code-insiders'; +import codeTunnelInsidersCompletionSpec from '../../completions/code-tunnel-insiders'; import type { ISuiteSpec } from '../helpers'; -import { createCodeTestSpecs } from './code.test'; +import { createCodeTestSpecs, createCodeTunnelTestSpecs } from './code.test'; export const codeInsidersTestSuite: ISuiteSpec = { name: 'code-insiders', @@ -13,3 +14,11 @@ export const codeInsidersTestSuite: ISuiteSpec = { availableCommands: 'code-insiders', testSpecs: createCodeTestSpecs('code-insiders') }; + +export const codeTunnelInsidersTestSuite: ISuiteSpec = { + name: 'code-tunnel-insiders', + completionSpecs: codeTunnelInsidersCompletionSpec, + availableCommands: 'code-tunnel-insiders', + testSpecs: createCodeTunnelTestSpecs('code-tunnel-insiders') +}; + diff --git a/extensions/terminal-suggest/src/test/completions/code.test.ts b/extensions/terminal-suggest/src/test/completions/code.test.ts index b327e3e438e5..d00dcdbf947b 100644 --- a/extensions/terminal-suggest/src/test/completions/code.test.ts +++ b/extensions/terminal-suggest/src/test/completions/code.test.ts @@ -7,6 +7,7 @@ import 'mocha'; import codeCompletionSpec from '../../completions/code'; import { testPaths, type ISuiteSpec, type ITestSpec } from '../helpers'; import codeInsidersCompletionSpec from '../../completions/code-insiders'; +import codeTunnelCompletionSpec from '../../completions/code-tunnel'; export const codeSpecOptionsAndSubcommands = [ '-a ', @@ -104,9 +105,171 @@ export function createCodeTestSpecs(executable: string): ITestSpec[] { ]; } +export function createCodeTunnelTestSpecs(executable: string): ITestSpec[] { + const subcommandAndFlags: string[] = [ + '-', + '--add ', + '--category ', + '--cli-data-dir ', + '--diff ', + '--disable-extension ', + '--disable-extensions', + '--disable-gpu', + '--enable-proposed-api', + '--extensions-dir []', + '--goto ', + '--help', + '--inspect-brk-extensions ', + '--inspect-extensions ', + '--install-extension ', + '--list-extensions', + '--locale ', + '--locate-shell-integration-path ', + '--log []', + '--max-memory ', + '--merge ', + '--new-window', + '--pre-release', + '--prof-startup', + '--profile ', + '--reuse-window', + '--show-versions', + '--status', + '--sync ', + '--telemetry', + '--uninstall-extension ', + '--use-version []', + '--user-data-dir []', + '--verbose', + '--version', + '--wait', + '-a ', + '-d ', + '-g ', + '-h', + '-m ', + '-n', + '-r', + '-s', + '-v', + '-w', + 'ext', + 'help', + 'serve-web', + 'status', + 'tunnel', + 'version' + ]; + const tunnelSubcommandsAndFlags: string[] = [ + '--accept-server-license-terms', + '--cli-data-dir ', + '--extensions-dir []', + '--help', + '--install-extension []', + '--log []', + '--name []', + '--no-sleep', + '--random-name', + '--server-data-dir []', + '--use-version []', + '--user-data-dir []', + '--verbose', + '-h', + 'help', + 'kill', + 'prune', + 'rename ', + 'restart', + 'service', + 'status', + 'unregister', + 'user', + ]; + + const helpSubcommands: string[] = [ + 'help', + 'kill', + 'prune', + 'rename', + 'restart', + 'service', + 'status', + 'unregister', + 'user' + ]; + const serveWebSubcommandsAndFlags: string[] = [ + '--accept-server-license-terms', + '--cli-data-dir ', + '--connection-token []', + '--connection-token-file []', + '--help', + '--host []', + '--log []', + '--port []', + '--server-base-path []', + '--server-data-dir []', + '--socket-path []', + '--verbose', + '--without-connection-token', + '-h' + ]; + + const extSubcommands: string[] = [ + 'install []', + 'list', + 'uninstall []', + 'update' + ]; + + const commonFlags: string[] = [ + '--cli-data-dir ', + '--log []', + '--verbose', + '--help', + '-h' + ]; + + const typingTests: ITestSpec[] = []; + for (let i = 1; i < executable.length; i++) { + const expectedCompletions = [{ label: executable, description: executable === codeCompletionSpec.name || executable === codeTunnelCompletionSpec.name ? (codeCompletionSpec as any).description : (codeInsidersCompletionSpec as any).description }]; + const input = `${executable.slice(0, i)}|`; + typingTests.push({ input, expectedCompletions, expectedResourceRequests: input.endsWith(' ') ? undefined : { type: 'both', cwd: testPaths.cwd } }); + } + + return [ + ...typingTests, + { input: `${executable} |`, expectedCompletions: subcommandAndFlags, expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, + { input: `${executable} tunnel |`, expectedCompletions: tunnelSubcommandsAndFlags }, + { input: `${executable} tunnel user |`, expectedCompletions: ['help', 'login', 'logout', 'show'] }, + { input: `${executable} tunnel prune |`, expectedCompletions: [...commonFlags] }, + { input: `${executable} tunnel kill |`, expectedCompletions: [...commonFlags] }, + { input: `${executable} tunnel restart |`, expectedCompletions: [...commonFlags] }, + { input: `${executable} tunnel status |`, expectedCompletions: [...commonFlags] }, + { input: `${executable} tunnel rename |`, expectedCompletions: [...commonFlags] }, + { input: `${executable} tunnel unregister |`, expectedCompletions: [...commonFlags] }, + { input: `${executable} tunnel service |`, expectedCompletions: [...commonFlags, 'help', 'install', 'log', 'uninstall'] }, + { input: `${executable} tunnel help |`, expectedCompletions: helpSubcommands }, + { input: `${executable} serve-web |`, expectedCompletions: serveWebSubcommandsAndFlags }, + { input: `${executable} ext |`, expectedCompletions: extSubcommands }, + { input: `${executable} ext list |`, expectedCompletions: [...commonFlags, '--category []', '--show-versions'] }, + { input: `${executable} ext install |`, expectedCompletions: [...commonFlags, '--pre-release', '--donot-include-pack-and-dependencies', '--force'] }, + { input: `${executable} ext update |`, expectedCompletions: [...commonFlags] }, + { input: `${executable} status |`, expectedCompletions: commonFlags }, + { input: `${executable} version |`, expectedCompletions: commonFlags }, + + ]; +} + export const codeTestSuite: ISuiteSpec = { name: 'code', completionSpecs: codeCompletionSpec, availableCommands: 'code', testSpecs: createCodeTestSpecs('code') }; + +export const codeTunnelTestSuite: ISuiteSpec = { + name: 'code-tunnel', + completionSpecs: codeTunnelCompletionSpec, + availableCommands: 'code-tunnel', + testSpecs: createCodeTunnelTestSpecs('code-tunnel') +}; diff --git a/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts b/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts index 69b65b2002ca..85f9cb6ca981 100644 --- a/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts +++ b/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts @@ -9,9 +9,9 @@ import { basename } from 'path'; import { asArray, getCompletionItemsFromSpecs } from '../terminalSuggestMain'; import { getTokenType } from '../tokens'; import { cdTestSuiteSpec as cdTestSuite } from './completions/cd.test'; -import { codeSpecOptionsAndSubcommands, codeTestSuite } from './completions/code.test'; +import { codeSpecOptionsAndSubcommands, codeTestSuite, codeTunnelTestSuite } from './completions/code.test'; import { testPaths, type ISuiteSpec } from './helpers'; -import { codeInsidersTestSuite } from './completions/code-insiders.test'; +import { codeInsidersTestSuite, codeTunnelInsidersTestSuite } from './completions/code-insiders.test'; import { lsTestSuiteSpec } from './completions/upstream/ls.test'; import { echoTestSuiteSpec } from './completions/upstream/echo.test'; import { mkdirTestSuiteSpec } from './completions/upstream/mkdir.test'; @@ -43,6 +43,8 @@ const testSpecs2: ISuiteSpec[] = [ cdTestSuite, codeTestSuite, codeInsidersTestSuite, + codeTunnelTestSuite, + codeTunnelInsidersTestSuite, // completions/upstream/ echoTestSuiteSpec, diff --git a/extensions/tunnel-forwarding/package-lock.json b/extensions/tunnel-forwarding/package-lock.json index 57accdc3b8d8..307cef660711 100644 --- a/extensions/tunnel-forwarding/package-lock.json +++ b/extensions/tunnel-forwarding/package-lock.json @@ -16,19 +16,21 @@ } }, "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": "20.17.27", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.27.tgz", + "integrity": "sha512-U58sbKhDrthHlxHRJw7ZLiLDZGmAUOZUbpw0S6nL27sYUdhvgBLCRu/keSd6qcTsfArd1sRFCCBxzWATGr/0UA==", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.2" } }, "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.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" } } } diff --git a/extensions/typescript-language-features/package-lock.json b/extensions/typescript-language-features/package-lock.json index 4f97eb1048c4..2078b4845e8e 100644 --- a/extensions/typescript-language-features/package-lock.json +++ b/extensions/typescript-language-features/package-lock.json @@ -152,12 +152,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": "20.17.27", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.27.tgz", + "integrity": "sha512-U58sbKhDrthHlxHRJw7ZLiLDZGmAUOZUbpw0S6nL27sYUdhvgBLCRu/keSd6qcTsfArd1sRFCCBxzWATGr/0UA==", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.2" } }, "node_modules/@types/semver": { @@ -257,10 +258,11 @@ "integrity": "sha512-V+uqV66BOQnWxvI6HjDnE4VkInmYZUQ4dgB7gzaDyFyFSK1i1nF/j7DpS9UbQAgV9NaF1XpcyuavnM1qOeiEIg==" }, "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.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" }, "node_modules/vscode-tas-client": { "version": "0.1.84", diff --git a/extensions/vscode-api-tests/package-lock.json b/extensions/vscode-api-tests/package-lock.json index cd90b33ca496..9a80cf4f19bd 100644 --- a/extensions/vscode-api-tests/package-lock.json +++ b/extensions/vscode-api-tests/package-lock.json @@ -26,12 +26,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": "20.17.27", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.27.tgz", + "integrity": "sha512-U58sbKhDrthHlxHRJw7ZLiLDZGmAUOZUbpw0S6nL27sYUdhvgBLCRu/keSd6qcTsfArd1sRFCCBxzWATGr/0UA==", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.2" } }, "node_modules/@types/node-forge": { @@ -216,10 +217,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.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" }, "node_modules/wrap-ansi": { "version": "7.0.0", diff --git a/extensions/vscode-api-tests/src/utils.ts b/extensions/vscode-api-tests/src/utils.ts index 7223fa8a6a13..4bf024207157 100644 --- a/extensions/vscode-api-tests/src/utils.ts +++ b/extensions/vscode-api-tests/src/utils.ts @@ -24,7 +24,7 @@ export async function createRandomFile(contents: string | Uint8Array = '', dir: } else { fakeFile = vscode.Uri.parse(`${testFs.scheme}:/${rndName() + ext}`); } - testFs.writeFile(fakeFile, Buffer.from(contents), { create: true, overwrite: true }); + testFs.writeFile(fakeFile, typeof contents === 'string' ? Buffer.from(contents) : Buffer.from(contents), { create: true, overwrite: true }); return fakeFile; } diff --git a/extensions/vscode-test-resolver/package-lock.json b/extensions/vscode-test-resolver/package-lock.json index 367c4dca2e0e..3bdbeac9b3ed 100644 --- a/extensions/vscode-test-resolver/package-lock.json +++ b/extensions/vscode-test-resolver/package-lock.json @@ -16,19 +16,21 @@ } }, "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": "20.17.27", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.27.tgz", + "integrity": "sha512-U58sbKhDrthHlxHRJw7ZLiLDZGmAUOZUbpw0S6nL27sYUdhvgBLCRu/keSd6qcTsfArd1sRFCCBxzWATGr/0UA==", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.2" } }, "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.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" } } } diff --git a/package-lock.json b/package-lock.json index d26d71db3d7f..fa673d72cee8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "@types/semver": "^7.5.8", "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.0", - "@vscode/policy-watcher": "^1.3.0", + "@vscode/policy-watcher": "^1.3.1", "@vscode/proxy-agent": "^0.32.0", "@vscode/ripgrep": "^1.15.11", "@vscode/spdlog": "^0.15.0", @@ -97,7 +97,7 @@ "cssnano": "^6.0.3", "debounce": "^1.0.0", "deemon": "^1.8.0", - "electron": "34.3.2", + "electron": "34.4.0", "eslint": "^9.11.1", "eslint-formatter-compact": "^8.40.0", "eslint-plugin-header": "3.1.1", @@ -155,7 +155,7 @@ "ts-node": "^10.9.1", "tsec": "0.2.7", "tslib": "^2.6.3", - "typescript": "^5.8.0-dev.20250207", + "typescript": "^5.9.0-dev.20250324", "typescript-eslint": "^8.8.0", "util": "^0.12.4", "webpack": "^5.94.0", @@ -247,29 +247,31 @@ } }, "node_modules/@azure/core-http": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/@azure/core-http/-/core-http-2.2.2.tgz", - "integrity": "sha512-V1DdoO9V/sFimKpdWoNBgsE+QUjQgpXYnxrTdUp5RyhsTJjvEVn/HKmTQXIHuLUUo6IyIWj+B+Dg4VaXse9dIA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@azure/core-http/-/core-http-2.3.2.tgz", + "integrity": "sha512-Z4dfbglV9kNZO177CNx4bo5ekFuYwwsvjLiKdZI4r84bYGv3irrbQz7JC3/rUfFH2l4T/W6OFleJaa2X0IaQqw==", + "deprecated": "This package is no longer supported. Please migrate to use @azure/core-rest-pipeline", "dev": true, + "license": "MIT", "dependencies": { "@azure/abort-controller": "^1.0.0", - "@azure/core-asynciterator-polyfill": "^1.0.0", "@azure/core-auth": "^1.3.0", "@azure/core-tracing": "1.0.0-preview.13", + "@azure/core-util": "^1.1.1", "@azure/logger": "^1.0.0", "@types/node-fetch": "^2.5.0", "@types/tunnel": "^0.0.3", "form-data": "^4.0.0", - "node-fetch": "^2.6.0", + "node-fetch": "^2.6.7", "process": "^0.11.10", "tough-cookie": "^4.0.0", "tslib": "^2.2.0", "tunnel": "^0.0.6", "uuid": "^8.3.0", - "xml2js": "^0.4.19" + "xml2js": "^0.5.0" }, "engines": { - "node": ">=12.0.0" + "node": ">=14.0.0" } }, "node_modules/@azure/core-http/node_modules/@azure/abort-controller": { @@ -306,28 +308,6 @@ "uuid": "dist/bin/uuid" } }, - "node_modules/@azure/core-http/node_modules/xml2js": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", - "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", - "dev": true, - "dependencies": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/@azure/core-http/node_modules/xmlbuilder": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, "node_modules/@azure/core-lro": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.2.1.tgz", @@ -482,80 +462,20 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.22.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", - "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/highlight": "^7.22.13", - "chalk": "^2.4.2" + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/code-frame/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/code-frame/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/code-frame/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/compat-data": { "version": "7.18.8", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.18.8.tgz", @@ -750,19 +670,21 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -777,100 +699,28 @@ } }, "node_modules/@babel/helpers": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.18.9.tgz", - "integrity": "sha512-Jf5a+rbrLoR4eNdUmnFu8cN5eNJT6qdTdOg5IHIzq87WwyRw9PwguLFOWYgktN/60IP4fgDUawJvs7PjQIzELQ==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz", + "integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/template": "^7.18.6", - "@babel/traverse": "^7.18.9", - "@babel/types": "^7.18.9" + "@babel/template": "^7.27.0", + "@babel/types": "^7.27.0" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/highlight": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", - "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@babel/highlight/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/parser": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", - "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", + "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.0" + }, "bin": { "parser": "bin/babel-parser.js" }, @@ -879,14 +729,15 @@ } }, "node_modules/@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz", + "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.27.0", + "@babel/types": "^7.27.0" }, "engines": { "node": ">=6.9.0" @@ -923,14 +774,14 @@ } }, "node_modules/@babel/types": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", - "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", + "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1181,17 +1032,32 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.0.tgz", - "integrity": "sha512-vH9PiIMMwvhCx31Af3HiGzsVNULDbyVkHXwlemn/B0TFj/00ho3y55efXrUZTfQipxoHC5u4xq6zblww1zm1Ig==", + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.7.tgz", + "integrity": "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==", "dev": true, + "license": "Apache-2.0", "dependencies": { + "@eslint/core": "^0.12.0", "levn": "^0.4.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz", + "integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@gulp-sourcemaps/identity-map": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@gulp-sourcemaps/identity-map/-/identity-map-2.0.1.tgz", @@ -1646,18 +1512,20 @@ } }, "node_modules/@octokit/openapi-types": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-11.2.0.tgz", - "integrity": "sha512-PBsVO+15KSlGmiI8QAzaqvsNlZlrDlyAJYcrXBCvVUxCp7VnXjkwPoFHgjEJXx3WF9BAwkA6nfCUA7i9sODzKA==", - "dev": true + "version": "12.11.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-12.11.0.tgz", + "integrity": "sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ==", + "dev": true, + "license": "MIT" }, "node_modules/@octokit/plugin-paginate-rest": { - "version": "2.17.0", - "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.17.0.tgz", - "integrity": "sha512-tzMbrbnam2Mt4AhuyCHvpRkS0oZ5MvwwcQPYGtMv4tUa5kkzG58SVB0fcsLulOZQeRnOgdkZWkRUiyBlh0Bkyw==", + "version": "2.21.3", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.21.3.tgz", + "integrity": "sha512-aCZTEf0y2h3OLbrgKkrfFdjRL6eSOo8komneVQJnYecAxIej7Bafor2xhuDJOIFau4pk0i/P28/XgtbyPF0ZHw==", "dev": true, + "license": "MIT", "dependencies": { - "@octokit/types": "^6.34.0" + "@octokit/types": "^6.40.0" }, "peerDependencies": { "@octokit/core": ">=2" @@ -1723,12 +1591,13 @@ } }, "node_modules/@octokit/types": { - "version": "6.34.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.34.0.tgz", - "integrity": "sha512-s1zLBjWhdEI2zwaoSgyOFoKSl109CUcVBCc7biPJ3aAf6LGLU6szDvi31JPU7bxfla2lqfhjbbg/5DdFNxOwHw==", + "version": "6.41.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.41.0.tgz", + "integrity": "sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==", "dev": true, + "license": "MIT", "dependencies": { - "@octokit/openapi-types": "^11.2.0" + "@octokit/openapi-types": "^12.11.0" } }, "node_modules/@opentelemetry/api": { @@ -2436,12 +2305,13 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.14.13", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.13.tgz", - "integrity": "sha512-+bHoGiZb8UiQ0+WEtmph2IWQCjIqg8MDZMAV+ppRRhUZnquF5mQkP/9vpSwJClEiSM/C7fZZExPzfU0vJTyp8w==", + "version": "20.17.27", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.27.tgz", + "integrity": "sha512-U58sbKhDrthHlxHRJw7ZLiLDZGmAUOZUbpw0S6nL27sYUdhvgBLCRu/keSd6qcTsfArd1sRFCCBxzWATGr/0UA==", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.2" } }, "node_modules/@types/node-fetch": { @@ -2843,9 +2713,9 @@ } }, "node_modules/@vscode/policy-watcher": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@vscode/policy-watcher/-/policy-watcher-1.3.0.tgz", - "integrity": "sha512-a8pPxlZlMJWOOj2NZ/2ceXgHdDU/NXo+8Pn/InV/sPBfbvTnf/MpMc4pscm9pdU4UIrTGR5+OduQW7mTK8DK7Q==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@vscode/policy-watcher/-/policy-watcher-1.3.1.tgz", + "integrity": "sha512-StbJFalUanuWSIeWng0yxu9Rjiyi9BPsvS5UlPqwyRbyA08TLvrM88SHMO2DVfyzMDjwM4MogF8LEUBTvVxS8g==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -4167,49 +4037,80 @@ "dev": true }, "node_modules/bare-events": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.4.2.tgz", - "integrity": "sha512-qMKFd2qG/36aA4GwvKq8MxnPgCQAmBWmSyLWsJcbn8v03wvIPQ/hG1Ms8bPzndZxMDoHpxez5VOS+gC9Yi24/Q==", + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.4.tgz", + "integrity": "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==", "dev": true, + "license": "Apache-2.0", "optional": true }, "node_modules/bare-fs": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-2.3.1.tgz", - "integrity": "sha512-W/Hfxc/6VehXlsgFtbB5B4xFcsCl+pAh30cYhoFyXErf6oGrwjh8SwiPAdHgpmWonKuYpZgGywN0SXt7dgsADA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.0.2.tgz", + "integrity": "sha512-S5mmkMesiduMqnz51Bfh0Et9EX0aTCJxhsI4bvzFFLs8Z1AV8RDHadfY5CyLwdoLHgXbNBEN1gQcbEtGwuvixw==", "dev": true, + "license": "Apache-2.0", "optional": true, "dependencies": { - "bare-events": "^2.0.0", - "bare-path": "^2.0.0", - "bare-stream": "^2.0.0" + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } } }, "node_modules/bare-os": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-2.4.0.tgz", - "integrity": "sha512-v8DTT08AS/G0F9xrhyLtepoo9EJBJ85FRSMbu1pQUlAf6A8T0tEEQGMVObWeqpjhSPXsE0VGlluFBJu2fdoTNg==", + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.1.tgz", + "integrity": "sha512-uaIjxokhFidJP+bmmvKSgiMzj2sV5GPHaZVAIktcxcpCyBFFWO+YlikVAdhmUo2vYFvFhOXIAlldqV29L8126g==", "dev": true, - "optional": true + "license": "Apache-2.0", + "optional": true, + "engines": { + "bare": ">=1.14.0" + } }, "node_modules/bare-path": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-2.1.3.tgz", - "integrity": "sha512-lh/eITfU8hrj9Ru5quUp0Io1kJWIk1bTjzo7JH1P5dWmQ2EL4hFUlfI8FonAhSlgIfhn63p84CDY/x+PisgcXA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", "dev": true, + "license": "Apache-2.0", "optional": true, "dependencies": { - "bare-os": "^2.1.0" + "bare-os": "^3.0.1" } }, "node_modules/bare-stream": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.1.3.tgz", - "integrity": "sha512-tiDAH9H/kP+tvNO5sczyn9ZAA7utrSMobyDchsnyyXBuUe2FSQWbxhtuHB8jwpHYYevVo2UJpcmvvjrbHboUUQ==", + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.6.5.tgz", + "integrity": "sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==", "dev": true, + "license": "Apache-2.0", "optional": true, "dependencies": { - "streamx": "^2.18.0" + "streamx": "^2.21.0" + }, + "peerDependencies": { + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } } }, "node_modules/base": { @@ -6079,10 +5980,11 @@ } }, "node_modules/editorconfig/node_modules/@types/node": { - "version": "10.12.21", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.21.tgz", - "integrity": "sha512-CBgLNk4o3XMnqMc0rhb6lc77IwShMEglz05deDcn2lQxyXEZivfwgYJu7SMha9V5XcrP6qZuevTHV/QrN2vjKQ==", - "dev": true + "version": "10.17.60", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", + "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==", + "dev": true, + "license": "MIT" }, "node_modules/editorconfig/node_modules/@types/semver": { "version": "5.5.0", @@ -6112,9 +6014,9 @@ "dev": true }, "node_modules/electron": { - "version": "34.3.2", - "resolved": "https://registry.npmjs.org/electron/-/electron-34.3.2.tgz", - "integrity": "sha512-n9tzmFexVLxipZXwMTY30H10f0X9k2OP0SkpSwL5VvnDZi0l/Hc+8CEArKkQPbbSf/IS7nxgc96gtTaR+XoSBg==", + "version": "34.4.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-34.4.0.tgz", + "integrity": "sha512-/+RKCgdXa3yp/Ui+xkA84YJMZjnDyAhlHpVwpM454o+ELX3c3UloMzaCt2J0Cq+csAyPakrpDyGIQEabLUeU3w==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -10922,7 +10824,8 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/js-yaml": { "version": "4.1.0", @@ -11687,8 +11590,9 @@ "node_modules/matchdep": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/matchdep/-/matchdep-2.0.0.tgz", - "integrity": "sha1-xvNINKDY28OzfCfui7yyfHd1WC4= sha512-LFgVbaHIHMqCRuCZyfCtUOq9/Lnzhi7Z0KFUE2fhD54+JN2jLh3hC02RLkqauJ3U4soU6H1J3tfj/Byk7GoEjA==", + "integrity": "sha512-LFgVbaHIHMqCRuCZyfCtUOq9/Lnzhi7Z0KFUE2fhD54+JN2jLh3hC02RLkqauJ3U4soU6H1J3tfj/Byk7GoEjA==", "dev": true, + "license": "MIT", "dependencies": { "findup-sync": "^2.0.0", "micromatch": "^3.0.4", @@ -11762,8 +11666,9 @@ "node_modules/matchdep/node_modules/findup-sync": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-2.0.0.tgz", - "integrity": "sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw= sha512-vs+3unmJT45eczmcAZ6zMJtxN3l/QXeccaXQx5cu/MeJMhewVfoWZqibRkOxPnmoR59+Zy5hjabfQc6JLSah4g==", + "integrity": "sha512-vs+3unmJT45eczmcAZ6zMJtxN3l/QXeccaXQx5cu/MeJMhewVfoWZqibRkOxPnmoR59+Zy5hjabfQc6JLSah4g==", "dev": true, + "license": "MIT", "dependencies": { "detect-file": "^1.0.0", "is-glob": "^3.1.0", @@ -14615,9 +14520,10 @@ } }, "node_modules/prebuild-install/node_modules/tar-fs": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", - "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz", + "integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==", + "license": "MIT", "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", @@ -14803,12 +14709,6 @@ "inherits": "~2.0.0" } }, - "node_modules/queue-tick": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", - "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", - "dev": true - }, "node_modules/quick-lru": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", @@ -16474,13 +16374,13 @@ } }, "node_modules/streamx": { - "version": "2.18.0", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.18.0.tgz", - "integrity": "sha512-LLUC1TWdjVdn1weXGcSxyTR3T4+acB6tVGXT95y0nGbca4t4o/ng1wKAGTljm9VicuCVLvRlqFYXYy5GwgM7sQ==", + "version": "2.22.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.0.tgz", + "integrity": "sha512-sLh1evHOzBy/iWRiR6d1zRcLao4gGZr3C1kzNz4fopCOKJb6xD9ub8Mpi9Mr1R6id5o43S+d93fI48UC5uM9aw==", "dev": true, + "license": "MIT", "dependencies": { "fast-fifo": "^1.3.2", - "queue-tick": "^1.0.1", "text-decoder": "^1.1.0" }, "optionalDependencies": { @@ -16889,17 +16789,18 @@ } }, "node_modules/tar-fs": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz", - "integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==", + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.8.tgz", + "integrity": "sha512-ZoROL70jptorGAlgAYiLoBLItEKw/fUxg9BSYK/dF/GAGYFJOJJJMvjPAKDJraCXFwadD456FCuvLWgfhMsPwg==", "dev": true, + "license": "MIT", "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" }, "optionalDependencies": { - "bare-fs": "^2.1.1", - "bare-path": "^2.1.0" + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" } }, "node_modules/tar-fs/node_modules/pump": { @@ -17185,15 +17086,6 @@ "node": ">=0.10.0" } }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/to-object-path": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", @@ -17562,9 +17454,9 @@ "dev": true }, "node_modules/typescript": { - "version": "5.8.0-dev.20250207", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.0-dev.20250207.tgz", - "integrity": "sha512-bRCO1GkVxTLd/UFJWOg9R1oRiSMidcfpICzuQlDJlHspv6hlcJvvIJP0BvQxrBYpu4dbzqp/Fh8rRYIkEjbSlQ==", + "version": "5.9.0-dev.20250324", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.0-dev.20250324.tgz", + "integrity": "sha512-qehVULhODb1IphPefX/jq0wAXpk6ifAt2lF3mxTsyR3yPDoPkiQdvxw9cURaDjJw6vCijSER6dU8kWmHdWaQnA==", "dev": true, "license": "Apache-2.0", "bin": { @@ -17753,10 +17645,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.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" }, "node_modules/union-value": { "version": "1.0.1", diff --git a/package.json b/package.json index 50c72719705a..9b0d0b1855e4 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.100.0", - "distro": "d42d927c558e17ea6e489cff62878654737d85f7", + "distro": "2155257cf9c35fe3ca94480e3c41859d01fdb988", "author": { "name": "Microsoft Corporation" }, @@ -76,7 +76,7 @@ "@types/semver": "^7.5.8", "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.0", - "@vscode/policy-watcher": "^1.3.0", + "@vscode/policy-watcher": "^1.3.1", "@vscode/proxy-agent": "^0.32.0", "@vscode/ripgrep": "^1.15.11", "@vscode/spdlog": "^0.15.0", @@ -156,7 +156,7 @@ "cssnano": "^6.0.3", "debounce": "^1.0.0", "deemon": "^1.8.0", - "electron": "34.3.2", + "electron": "34.4.0", "eslint": "^9.11.1", "eslint-formatter-compact": "^8.40.0", "eslint-plugin-header": "3.1.1", @@ -214,7 +214,7 @@ "ts-node": "^10.9.1", "tsec": "0.2.7", "tslib": "^2.6.3", - "typescript": "^5.8.0-dev.20250207", + "typescript": "^5.9.0-dev.20250324", "typescript-eslint": "^8.8.0", "util": "^0.12.4", "webpack": "^5.94.0", diff --git a/remote/package-lock.json b/remote/package-lock.json index 7b52643c3f32..f9921d3e5210 100644 --- a/remote/package-lock.json +++ b/remote/package-lock.json @@ -1373,9 +1373,10 @@ } }, "node_modules/tar-fs": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", - "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz", + "integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==", + "license": "MIT", "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", diff --git a/src/tsconfig.base.json b/src/tsconfig.base.json index e354b0ed463c..6d2769496488 100644 --- a/src/tsconfig.base.json +++ b/src/tsconfig.base.json @@ -7,6 +7,7 @@ "noImplicitReturns": true, "noImplicitOverride": true, "noUnusedLocals": true, + "noUncheckedSideEffectImports": true, "allowUnreachableCode": false, "strict": true, "exactOptionalPropertyTypes": false, diff --git a/src/tsconfig.monaco.json b/src/tsconfig.monaco.json index d64d6bd8c8e7..cad1e06383d7 100644 --- a/src/tsconfig.monaco.json +++ b/src/tsconfig.monaco.json @@ -17,6 +17,7 @@ "declaration": true }, "include": [ + "typings/css.d.ts", "typings/thenable.d.ts", "typings/vscode-globals-product.d.ts", "typings/vscode-globals-nls.d.ts", diff --git a/src/typings/css.d.ts b/src/typings/css.d.ts new file mode 100644 index 000000000000..abb669217814 --- /dev/null +++ b/src/typings/css.d.ts @@ -0,0 +1,9 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + + +// Recognize all CSS files as valid module imports +declare module "vs/css!*" { } +declare module "*.css" { } diff --git a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf b/src/vs/base/browser/ui/codicons/codicon/codicon.ttf index 872f328858f8..70d74b87fe18 100644 Binary files a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf and b/src/vs/base/browser/ui/codicons/codicon/codicon.ttf differ diff --git a/src/vs/base/common/arrays.ts b/src/vs/base/common/arrays.ts index 98bf168f39da..d72f7e904f67 100644 --- a/src/vs/base/common/arrays.ts +++ b/src/vs/base/common/arrays.ts @@ -731,13 +731,17 @@ export function compareUndefinedSmallest(comparator: Comparator): Comparat } export class ArrayQueue { + private readonly items: readonly T[]; private firstIdx = 0; - private lastIdx = this.items.length - 1; + private lastIdx: number; /** * Constructs a queue that is backed by the given array. Runtime is O(1). */ - constructor(private readonly items: readonly T[]) { } + constructor(items: readonly T[]) { + this.items = items; + this.lastIdx = this.items.length - 1; + } get length(): number { return this.lastIdx - this.firstIdx + 1; diff --git a/src/vs/base/common/codiconsLibrary.ts b/src/vs/base/common/codiconsLibrary.ts index 13a3a4f0c310..34f4542a0552 100644 --- a/src/vs/base/common/codiconsLibrary.ts +++ b/src/vs/base/common/codiconsLibrary.ts @@ -592,4 +592,5 @@ export const codiconsLibrary = { flag: register('flag', 0xec3f), lightbulbEmpty: register('lightbulb-empty', 0xec40), symbolMethodArrow: register('symbol-method-arrow', 0xec41), + copilotUnavailable: register('copilot-unavailable', 0xec42), } as const; diff --git a/src/vs/base/common/observableInternal/index.ts b/src/vs/base/common/observableInternal/index.ts index c86501e702aa..3822ae0cf956 100644 --- a/src/vs/base/common/observableInternal/index.ts +++ b/src/vs/base/common/observableInternal/index.ts @@ -6,12 +6,12 @@ // This is a facade for the observable implementation. Only import from here! export { observableValueOpts } from './api.js'; -export { autorun, autorunDelta, autorunHandleChanges, autorunOpts, autorunWithStore, autorunWithStoreHandleChanges } from './autorun.js'; +export { autorun, autorunDelta, autorunHandleChanges, autorunOpts, autorunWithStore, autorunWithStoreHandleChanges, autorunIterableDelta } from './autorun.js'; export { asyncTransaction, disposableObservableValue, globalTransaction, observableValue, subtransaction, transaction, TransactionImpl, type IChangeContext, type IChangeTracker, type IObservable, type IObservableWithChange, type IObserver, type IReader, type ISettable, type ISettableObservable, type ITransaction, } from './base.js'; export { derived, derivedDisposable, derivedHandleChanges, derivedOpts, derivedWithSetter, derivedWithStore } from './derived.js'; export { ObservableLazy, ObservableLazyPromise, ObservablePromise, PromiseResult, } from './promise.js'; export { derivedWithCancellationToken, waitForState } from './utilsCancellation.js'; -export { constObservable, debouncedObservableDeprecated, derivedConstOnceDefined, derivedObservableWithCache, derivedObservableWithWritableCache, keepObserved, latestChangedValue, mapObservableArrayCached, observableFromEvent, observableFromEventOpts, observableFromPromise, observableFromValueWithChangeEvent, observableSignal, observableSignalFromEvent, recomputeInitiallyAndOnChange, runOnChange, runOnChangeWithStore, signalFromObservable, ValueWithChangeEventFromObservable, wasEventTriggeredRecently, type IObservableSignal, } from './utils.js'; +export { constObservable, debouncedObservableDeprecated, debouncedObservable, derivedConstOnceDefined, derivedObservableWithCache, derivedObservableWithWritableCache, keepObserved, latestChangedValue, mapObservableArrayCached, observableFromEvent, observableFromEventOpts, observableFromPromise, observableFromValueWithChangeEvent, observableSignal, observableSignalFromEvent, recomputeInitiallyAndOnChange, runOnChange, runOnChangeWithStore, runOnChangeWithCancellationToken, signalFromObservable, ValueWithChangeEventFromObservable, wasEventTriggeredRecently, type IObservableSignal, } from './utils.js'; export { type DebugOwner } from './debugName.js'; import { addLogger, setLogObservableFn } from './logging/logging.js'; diff --git a/src/vs/base/common/observableInternal/utils.ts b/src/vs/base/common/observableInternal/utils.ts index 530474b334ea..7e3580edead2 100644 --- a/src/vs/base/common/observableInternal/utils.ts +++ b/src/vs/base/common/observableInternal/utils.ts @@ -9,6 +9,7 @@ import { DebugNameData, DebugOwner, IDebugNameData, getDebugName, } from './debu import { BugIndicatingError, DisposableStore, EqualityComparer, Event, IDisposable, IValueWithChangeEvent, strictEquals, toDisposable } from './commonFacade/deps.js'; import { derived, derivedOpts } from './derived.js'; import { getLogger } from './logging/logging.js'; +import { CancellationToken, cancelOnDispose } from '../cancellation.js'; /** * Represents an efficient observable whose value never changes. @@ -674,3 +675,9 @@ export function runOnChangeWithStore(observable: IObservableWithChan } }; } + +export function runOnChangeWithCancellationToken(observable: IObservableWithChange, cb: (value: T, previousValue: undefined | T, deltas: RemoveUndefined[], token: CancellationToken) => Promise): IDisposable { + return runOnChangeWithStore(observable, (value, previousValue: undefined | T, deltas, store) => { + cb(value, previousValue, deltas, cancelOnDispose(store)); + }); +} diff --git a/src/vs/base/common/strings.ts b/src/vs/base/common/strings.ts index 1a8467170b55..1b12d1a42124 100644 --- a/src/vs/base/common/strings.ts +++ b/src/vs/base/common/strings.ts @@ -266,6 +266,14 @@ export function splitLinesIncludeSeparators(str: string): string[] { return linesWithSeparators; } +export function indexOfPattern(str: string, re: RegExp) { + const match = re.exec(str); + if (match) { + return match.index; + } + return -1; +} + /** * Returns first index of the string that is not whitespace. * If string is empty or contains only whitespaces, returns -1 diff --git a/src/vs/base/test/common/observable.test.ts b/src/vs/base/test/common/observable.test.ts index 26b0517c740d..dc5f931a9ae8 100644 --- a/src/vs/base/test/common/observable.test.ts +++ b/src/vs/base/test/common/observable.test.ts @@ -8,6 +8,7 @@ import { setUnexpectedErrorHandler } from '../../common/errors.js'; import { Emitter, Event } from '../../common/event.js'; import { DisposableStore } from '../../common/lifecycle.js'; import { autorun, autorunHandleChanges, derived, derivedDisposable, IObservable, IObserver, ISettableObservable, ITransaction, keepObserved, observableFromEvent, observableSignal, observableValue, transaction, waitForState } from '../../common/observable.js'; +// eslint-disable-next-line local/code-no-deep-import-of-internal import { BaseObservable, IObservableWithChange } from '../../common/observableInternal/base.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from './utils.js'; diff --git a/src/vs/editor/browser/gpu/objectCollectionBuffer.ts b/src/vs/editor/browser/gpu/objectCollectionBuffer.ts index a61fe8c09de7..9a918e1abcbc 100644 --- a/src/vs/editor/browser/gpu/objectCollectionBuffer.ts +++ b/src/vs/editor/browser/gpu/objectCollectionBuffer.ts @@ -20,7 +20,7 @@ export interface IObjectCollectionBuffer extends Disposable implements IObjectCollectionBuffer { - buffer: ArrayBuffer; + buffer: ArrayBufferLike; view: Float32Array; get bufferUsedSize() { diff --git a/src/vs/editor/browser/services/hoverService/hover.css b/src/vs/editor/browser/services/hoverService/hover.css index 8d483a541633..d7c20fbcde98 100644 --- a/src/vs/editor/browser/services/hoverService/hover.css +++ b/src/vs/editor/browser/services/hoverService/hover.css @@ -18,6 +18,12 @@ box-shadow: 0 2px 8px var(--vscode-widget-shadow); } +.monaco-workbench .workbench-hover .monaco-action-bar .action-item .codicon { + /* Given our font-size, adjust action icons accordingly */ + width: 13px; + height: 13px; +} + .monaco-workbench .workbench-hover hr { border-bottom: none; } @@ -26,6 +32,12 @@ font-size: 12px; } +.monaco-workbench .workbench-hover.compact .monaco-action-bar .action-item .codicon { + /* Given our font-size, adjust action icons accordingly */ + width: 12px; + height: 12px; +} + .monaco-workbench .workbench-hover.compact .hover-contents { padding: 2px 8px; } diff --git a/src/vs/editor/common/core/columnRange.ts b/src/vs/editor/common/core/columnRange.ts new file mode 100644 index 000000000000..e955587a294a --- /dev/null +++ b/src/vs/editor/common/core/columnRange.ts @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * 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'; +import { Range } from './range.js'; + +/** + * Represents a 1-based range of columns. + * Use {@lik OffsetRange} to represent a 0-based range. +*/ +export class ColumnRange { + public static fromOffsetRange(offsetRange: OffsetRange): ColumnRange { + return new ColumnRange(offsetRange.start + 1, offsetRange.endExclusive + 1); + } + + constructor( + /** 1-based */ + public readonly startColumn: number, + public readonly endColumnExclusive: number + ) { + if (startColumn > endColumnExclusive) { + throw new BugIndicatingError(`startColumn ${startColumn} cannot be after endColumnExclusive ${endColumnExclusive}`); + } + } + + toRange(lineNumber: number): Range { + return new Range(lineNumber, this.startColumn, lineNumber, this.endColumnExclusive); + } + + equals(other: ColumnRange): boolean { + return this.startColumn === other.startColumn + && this.endColumnExclusive === other.endColumnExclusive; + } + + toZeroBasedOffsetRange(): OffsetRange { + return new OffsetRange(this.startColumn - 1, this.endColumnExclusive - 1); + } +} diff --git a/src/vs/editor/common/core/rangeSingleLine.ts b/src/vs/editor/common/core/rangeSingleLine.ts new file mode 100644 index 000000000000..d16cf24b0dd2 --- /dev/null +++ b/src/vs/editor/common/core/rangeSingleLine.ts @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ColumnRange } from './columnRange.js'; +import { Range } from './range.js'; + +export class RangeSingleLine { + public static fromRange(range: Range): RangeSingleLine | undefined { + if (range.endLineNumber !== range.startLineNumber) { + return undefined; + } + return new RangeSingleLine(range.startLineNumber, new ColumnRange(range.startColumn, range.endColumn)); + } + + constructor( + /** 1-based */ + public readonly lineNumber: number, + public readonly columnRange: ColumnRange, + ) { } + + toRange(): Range { + return new Range(this.lineNumber, this.columnRange.startColumn, this.lineNumber, this.columnRange.endColumnExclusive); + } +} diff --git a/src/vs/editor/common/tokens/contiguousTokensEditing.ts b/src/vs/editor/common/tokens/contiguousTokensEditing.ts index 28d444a2df00..7da48f7527b1 100644 --- a/src/vs/editor/common/tokens/contiguousTokensEditing.ts +++ b/src/vs/editor/common/tokens/contiguousTokensEditing.ts @@ -135,10 +135,10 @@ export class ContiguousTokensEditing { } } -export function toUint32Array(arr: Uint32Array | ArrayBuffer): Uint32Array { +export function toUint32Array(arr: Uint32Array | ArrayBuffer): Uint32Array { if (arr instanceof Uint32Array) { - return arr; + return arr as Uint32Array; } else { - return new Uint32Array(arr); + return new Uint32Array(arr); } } diff --git a/src/vs/editor/common/tokens/tokenArray.ts b/src/vs/editor/common/tokens/tokenArray.ts index bc089d1604ae..555e5654dcc6 100644 --- a/src/vs/editor/common/tokens/tokenArray.ts +++ b/src/vs/editor/common/tokens/tokenArray.ts @@ -77,6 +77,11 @@ export class TokenArray { } return TokenArray.create(result); } + + public append(other: TokenArray): TokenArray { + const result: TokenInfo[] = this._tokenInfo.concat(other._tokenInfo); + return TokenArray.create(result); + } } export type TokenMetadata = number; diff --git a/src/vs/editor/common/tokens/tokenWithTextArray.ts b/src/vs/editor/common/tokens/tokenWithTextArray.ts new file mode 100644 index 000000000000..765d480a12b6 --- /dev/null +++ b/src/vs/editor/common/tokens/tokenWithTextArray.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 { OffsetRange } from '../core/offsetRange.js'; +import { ILanguageIdCodec } from '../languages.js'; +import { LineTokens } from './lineTokens.js'; + +/** + * This class represents a sequence of tokens. + * Conceptually, each token has a length and a metadata number. + * A token array might be used to annotate a string with metadata. + * Use {@link TokenWithTextArrayBuilder} to efficiently create a token array. + * + * TODO: Make this class more efficient (e.g. by using a Int32Array). +*/ +export class TokenWithTextArray { + public static fromLineTokens(lineTokens: LineTokens): TokenWithTextArray { + const tokenInfo: TokenWithTextInfo[] = []; + for (let i = 0; i < lineTokens.getCount(); i++) { + tokenInfo.push(new TokenWithTextInfo(lineTokens.getTokenText(i), lineTokens.getMetadata(i))); + } + return TokenWithTextArray.create(tokenInfo); + } + + public static create(tokenInfo: TokenWithTextInfo[]): TokenWithTextArray { + return new TokenWithTextArray(tokenInfo); + } + + private constructor( + private readonly _tokenInfo: TokenWithTextInfo[], + ) { } + + public toLineTokens(decoder: ILanguageIdCodec): LineTokens { + return LineTokens.createFromTextAndMetadata(this.map((_r, t) => ({ text: t.text, metadata: t.metadata })), decoder); + } + + public forEach(cb: (range: OffsetRange, tokenInfo: TokenWithTextInfo) => void): void { + let lengthSum = 0; + for (const tokenInfo of this._tokenInfo) { + const range = new OffsetRange(lengthSum, lengthSum + tokenInfo.text.length); + cb(range, tokenInfo); + lengthSum += tokenInfo.text.length; + } + } + + public map(cb: (range: OffsetRange, tokenInfo: TokenWithTextInfo) => T): T[] { + const result: T[] = []; + let lengthSum = 0; + for (const tokenInfo of this._tokenInfo) { + const range = new OffsetRange(lengthSum, lengthSum + tokenInfo.text.length); + result.push(cb(range, tokenInfo)); + lengthSum += tokenInfo.text.length; + } + return result; + } + + public slice(range: OffsetRange): TokenWithTextArray { + const result: TokenWithTextInfo[] = []; + let lengthSum = 0; + for (const tokenInfo of this._tokenInfo) { + const tokenStart = lengthSum; + const tokenEndEx = tokenStart + tokenInfo.text.length; + if (tokenEndEx > range.start) { + if (tokenStart >= range.endExclusive) { + break; + } + + const deltaBefore = Math.max(0, range.start - tokenStart); + const deltaAfter = Math.max(0, tokenEndEx - range.endExclusive); + + result.push(new TokenWithTextInfo(tokenInfo.text.slice(deltaBefore, tokenInfo.text.length - deltaAfter), tokenInfo.metadata)); + } + + lengthSum += tokenInfo.text.length; + } + return TokenWithTextArray.create(result); + } + + public append(other: TokenWithTextArray): TokenWithTextArray { + const result: TokenWithTextInfo[] = this._tokenInfo.concat(other._tokenInfo); + return TokenWithTextArray.create(result); + } +} + +export type TokenMetadata = number; + +export class TokenWithTextInfo { + constructor( + public readonly text: string, + public readonly metadata: TokenMetadata, + ) { } +} + +/** + * TODO: Make this class more efficient (e.g. by using a Int32Array). +*/ +export class TokenWithTextArrayBuilder { + private readonly _tokens: TokenWithTextInfo[] = []; + + public add(text: string, metadata: TokenMetadata): void { + this._tokens.push(new TokenWithTextInfo(text, metadata)); + } + + public build(): TokenWithTextArray { + return TokenWithTextArray.create(this._tokens); + } +} diff --git a/src/vs/editor/contrib/find/browser/findController.ts b/src/vs/editor/contrib/find/browser/findController.ts index 9e00a1d5d47a..5ce76b9ccc8e 100644 --- a/src/vs/editor/contrib/find/browser/findController.ts +++ b/src/vs/editor/contrib/find/browser/findController.ts @@ -29,7 +29,7 @@ import { KeybindingWeight } from '../../../../platform/keybinding/common/keybind import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; import { IQuickInputService } from '../../../../platform/quickinput/common/quickInput.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; -import { IThemeService, themeColorFromId } from '../../../../platform/theme/common/themeService.js'; +import { themeColorFromId } from '../../../../platform/theme/common/themeService.js'; import { Selection } from '../../../common/core/selection.js'; import { IHoverService } from '../../../../platform/hover/browser/hover.js'; import { FindWidgetSearchHistory } from './findWidgetSearchHistory.js'; @@ -452,7 +452,6 @@ export class FindController extends CommonFindController implements IFindControl @IContextViewService private readonly _contextViewService: IContextViewService, @IContextKeyService _contextKeyService: IContextKeyService, @IKeybindingService private readonly _keybindingService: IKeybindingService, - @IThemeService private readonly _themeService: IThemeService, @INotificationService notificationService: INotificationService, @IStorageService _storageService: IStorageService, @IClipboardService clipboardService: IClipboardService, @@ -514,7 +513,7 @@ export class FindController extends CommonFindController implements IFindControl } private _createFindWidget() { - this._widget = this._register(new FindWidget(this._editor, this, this._state, this._contextViewService, this._keybindingService, this._contextKeyService, this._themeService, this._storageService, this._notificationService, this._hoverService, this._findWidgetSearchHistory, this._replaceWidgetHistory)); + this._widget = this._register(new FindWidget(this._editor, this, this._state, this._contextViewService, this._keybindingService, this._contextKeyService, this._hoverService, this._findWidgetSearchHistory, this._replaceWidgetHistory)); this._findOptionsWidget = this._register(new FindOptionsWidget(this._editor, this._state, this._keybindingService)); } diff --git a/src/vs/editor/contrib/find/browser/findWidget.ts b/src/vs/editor/contrib/find/browser/findWidget.ts index e60addcd0846..24be44fddb5b 100644 --- a/src/vs/editor/contrib/find/browser/findWidget.ts +++ b/src/vs/editor/contrib/find/browser/findWidget.ts @@ -33,11 +33,9 @@ import { ContextScopedFindInput, ContextScopedReplaceInput } from '../../../../p import { showHistoryKeybindingHint } from '../../../../platform/history/browser/historyWidgetKeybindingHint.js'; import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; -import { INotificationService } from '../../../../platform/notification/common/notification.js'; -import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { asCssVariable, contrastBorder, editorFindMatchForeground, editorFindMatchHighlightBorder, editorFindMatchHighlightForeground, editorFindRangeHighlightBorder, inputActiveOptionBackground, inputActiveOptionBorder, inputActiveOptionForeground } from '../../../../platform/theme/common/colorRegistry.js'; import { registerIcon, widgetClose } from '../../../../platform/theme/common/iconRegistry.js'; -import { IThemeService, registerThemingParticipant } from '../../../../platform/theme/common/themeService.js'; +import { registerThemingParticipant } from '../../../../platform/theme/common/themeService.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { isHighContrast } from '../../../../platform/theme/common/theme.js'; import { assertIsDefined } from '../../../../base/common/types.js'; @@ -87,7 +85,6 @@ let MAX_MATCHES_COUNT_WIDTH = 69; // let FIND_ALL_CONTROLS_WIDTH = 17/** Find Input margin-left */ + (MAX_MATCHES_COUNT_WIDTH + 3 + 1) /** Match Results */ + 23 /** Button */ * 4 + 2/** sash */; const FIND_INPUT_AREA_HEIGHT = 33; // The height of Find Widget when Replace Input is not visible. -const ctrlEnterReplaceAllWarningPromptedKey = 'ctrlEnterReplaceAll.windows.donotask'; const ctrlKeyMod = (platform.isMacintosh ? KeyMod.WinCtrl : KeyMod.CtrlCmd); export class FindWidgetViewZone implements IViewZone { @@ -130,8 +127,6 @@ export class FindWidget extends Widget implements IOverlayWidget, IVerticalSashL private readonly _contextViewProvider: IContextViewProvider; private readonly _keybindingService: IKeybindingService; private readonly _contextKeyService: IContextKeyService; - private readonly _storageService: IStorageService; - private readonly _notificationService: INotificationService; private _domNode!: HTMLElement; private _cachedHeight: number | null = null; @@ -150,7 +145,6 @@ export class FindWidget extends Widget implements IOverlayWidget, IVerticalSashL private _isVisible: boolean; private _isReplaceVisible: boolean; private _ignoreChangeEvent: boolean; - private _ctrlEnterReplaceAllWarningPrompted: boolean; private readonly _findFocusTracker: dom.IFocusTracker; private readonly _findInputFocused: IContextKey; @@ -170,9 +164,6 @@ export class FindWidget extends Widget implements IOverlayWidget, IVerticalSashL contextViewProvider: IContextViewProvider, keybindingService: IKeybindingService, contextKeyService: IContextKeyService, - themeService: IThemeService, - storageService: IStorageService, - notificationService: INotificationService, private readonly _hoverService: IHoverService, private readonly _findWidgetSearchHistory: IHistory | undefined, private readonly _replaceWidgetHistory: IHistory | undefined, @@ -184,10 +175,6 @@ export class FindWidget extends Widget implements IOverlayWidget, IVerticalSashL this._contextViewProvider = contextViewProvider; this._keybindingService = keybindingService; this._contextKeyService = contextKeyService; - this._storageService = storageService; - this._notificationService = notificationService; - - this._ctrlEnterReplaceAllWarningPrompted = !!storageService.getBoolean(ctrlEnterReplaceAllWarningPromptedKey, StorageScope.PROFILE); this._isVisible = false; this._isReplaceVisible = false; @@ -879,17 +866,6 @@ export class FindWidget extends Widget implements IOverlayWidget, IVerticalSashL e.preventDefault(); return; } else { - if (platform.isWindows && platform.isNative && !this._ctrlEnterReplaceAllWarningPrompted) { - // this is the first time when users press Ctrl + Enter to replace all - this._notificationService.info( - nls.localize('ctrlEnter.keybindingChanged', - 'Ctrl+Enter now inserts line break instead of replacing all. You can modify the keybinding for editor.action.replaceAll to override this behavior.') - ); - - this._ctrlEnterReplaceAllWarningPrompted = true; - this._storageService.store(ctrlEnterReplaceAllWarningPromptedKey, true, StorageScope.PROFILE, StorageTarget.USER); - } - this._replaceInput.inputBox.insertAtCursor('\n'); e.preventDefault(); return; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/ghostText.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/ghostText.ts index 700bc76bf640..26fd510b5188 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/ghostText.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/ghostText.ts @@ -10,7 +10,7 @@ import { Range } from '../../../../common/core/range.js'; import { SingleTextEdit, TextEdit } from '../../../../common/core/textEdit.js'; import { LineDecoration } from '../../../../common/viewLayout/lineDecorations.js'; import { InlineDecoration } from '../../../../common/viewModel.js'; -import { ColumnRange } from '../utils.js'; +import { ColumnRange } from '../../../../common/core/columnRange.js'; export class GhostText { constructor( diff --git a/src/vs/editor/contrib/inlineCompletions/browser/utils.ts b/src/vs/editor/contrib/inlineCompletions/browser/utils.ts index 3d0da6db7e29..b451aff8fd79 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/utils.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/utils.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { Permutation, compareBy } from '../../../../base/common/arrays.js'; -import { BugIndicatingError } from '../../../../base/common/errors.js'; import { DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js'; import { IObservable, observableValue, ISettableObservable, autorun, transaction, IReader } from '../../../../base/common/observable.js'; import { ContextKeyValue, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; @@ -19,26 +18,6 @@ export function getReadonlyEmptyArray(): readonly T[] { return array; } -export class ColumnRange { - constructor( - public readonly startColumn: number, - public readonly endColumnExclusive: number, - ) { - if (startColumn > endColumnExclusive) { - throw new BugIndicatingError(`startColumn ${startColumn} cannot be after endColumnExclusive ${endColumnExclusive}`); - } - } - - toRange(lineNumber: number): Range { - return new Range(lineNumber, this.startColumn, lineNumber, this.endColumnExclusive); - } - - equals(other: ColumnRange): boolean { - return this.startColumn === other.startColumn - && this.endColumnExclusive === other.endColumnExclusive; - } -} - export function addPositions(pos1: Position, pos2: Position): Position { return new Position(pos1.lineNumber + pos2.lineNumber - 1, pos2.lineNumber === 1 ? pos1.column + pos2.column - 1 : pos2.column); } 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 d592eca09ea4..a4215f06f8e9 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts @@ -27,11 +27,13 @@ import { LineDecoration } from '../../../../../common/viewLayout/lineDecorations import { RenderLineInput, renderViewLine } from '../../../../../common/viewLayout/viewLineRenderer.js'; import { InlineDecorationType } from '../../../../../common/viewModel.js'; import { GhostText, GhostTextReplacement, IGhostTextLine } from '../../model/ghostText.js'; -import { ColumnRange } from '../../utils.js'; +import { RangeSingleLine } from '../../../../../common/core/rangeSingleLine.js'; +import { ColumnRange } from '../../../../../common/core/columnRange.js'; import { addDisposableListener, getWindow, isHTMLElement, n } from '../../../../../../base/browser/dom.js'; import './ghostTextView.css'; import { IMouseEvent, StandardMouseEvent } from '../../../../../../base/browser/mouseEvent.js'; import { CodeEditorWidget } from '../../../../../browser/widget/codeEditor/codeEditorWidget.js'; +import { TokenWithTextArray } from '../../../../../common/tokens/tokenWithTextArray.js'; export interface IGhostTextWidgetModel { readonly targetTextModel: IObservable; @@ -170,7 +172,7 @@ export class GhostTextView extends Disposable { const syntaxHighlightingEnabled = this._useSyntaxHighlighting.read(reader); const extraClassNames = this._extraClassNames.read(reader); - const { inlineTexts, additionalLines, hiddenRange } = computeGhostTextViewData(ghostText, textModel, GHOST_TEXT_CLASS_NAME + extraClassNames); + const { inlineTexts, additionalLines, hiddenRange, additionalLinesOriginalSuffix } = computeGhostTextViewData(ghostText, textModel, GHOST_TEXT_CLASS_NAME + extraClassNames); const currentLine = textModel.getLineContent(ghostText.lineNumber); const edit = new OffsetEdit(inlineTexts.map(t => SingleOffsetEdit.insert(t.column - 1, t.text))); @@ -178,10 +180,18 @@ export class GhostTextView extends Disposable { const newRanges = edit.getNewTextRanges(); const inlineTextsWithTokens = inlineTexts.map((t, idx) => ({ ...t, tokens: tokens?.[0]?.getTokensInRange(newRanges[idx]) })); - const tokenizedAdditionalLines: LineData[] = additionalLines.map((l, idx) => ({ - content: tokens?.[idx + 1] ?? LineTokens.createEmpty(l.content, this._languageService.languageIdCodec), - decorations: l.decorations, - })); + 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, @@ -344,8 +354,9 @@ function computeGhostTextViewData(ghostText: GhostText | GhostTextReplacement, t lastIdx = part.column - 1; } + let additionalLinesOriginalSuffix: RangeSingleLine | undefined = undefined; if (hiddenTextStartColumn !== undefined) { - addToAdditionalLines([{ line: textBufferLine.substring(lastIdx), lineDecorations: [] }], undefined); + additionalLinesOriginalSuffix = new RangeSingleLine(ghostText.lineNumber, new ColumnRange(lastIdx + 1, textBufferLine.length + 1)); } const hiddenRange = hiddenTextStartColumn !== undefined ? new ColumnRange(hiddenTextStartColumn, textBufferLine.length + 1) : undefined; @@ -354,6 +365,7 @@ function computeGhostTextViewData(ghostText: GhostText | GhostTextReplacement, t inlineTexts, additionalLines, hiddenRange, + additionalLinesOriginalSuffix, }; } 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 751570348696..b3713077a5a5 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 @@ -5,16 +5,13 @@ import { n, trackFocus } from '../../../../../../../base/browser/dom.js'; import { renderIcon } from '../../../../../../../base/browser/ui/iconLabel/iconLabels.js'; -import { timeout } from '../../../../../../../base/common/async.js'; import { Codicon } from '../../../../../../../base/common/codicons.js'; import { BugIndicatingError } from '../../../../../../../base/common/errors.js'; -import { Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../../../../../base/common/lifecycle.js'; -import { IObservable, ISettableObservable, autorun, autorunWithStore, constObservable, derived, observableFromEvent, observableValue, runOnChange } from '../../../../../../../base/common/observable.js'; -import { debouncedObservable } from '../../../../../../../base/common/observableInternal/utils.js'; +import { Disposable, DisposableStore, toDisposable } from '../../../../../../../base/common/lifecycle.js'; +import { IObservable, ISettableObservable, autorun, constObservable, debouncedObservable, derived, observableFromEvent, observableValue, runOnChange } from '../../../../../../../base/common/observable.js'; import { IAccessibilityService } from '../../../../../../../platform/accessibility/common/accessibility.js'; import { IHoverService } from '../../../../../../../platform/hover/browser/hover.js'; import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js'; -import { IStorageService, StorageScope, StorageTarget } from '../../../../../../../platform/storage/common/storage.js'; import { asCssVariable } from '../../../../../../../platform/theme/common/colorUtils.js'; import { IThemeService } from '../../../../../../../platform/theme/common/themeService.js'; import { IEditorMouseEvent } from '../../../../../../browser/editorBrowser.js'; @@ -27,19 +24,11 @@ import { EditorOption, RenderLineNumbersType } from '../../../../../../common/co import { LineRange } from '../../../../../../common/core/lineRange.js'; import { OffsetRange } from '../../../../../../common/core/offsetRange.js'; import { StickyScrollController } from '../../../../../stickyScroll/browser/stickyScrollController.js'; -import { InlineEditHost } from '../inlineEditsModel.js'; import { IInlineEditModel, InlineEditTabAction } from '../inlineEditsViewInterface.js'; import { getEditorBlendedColor, inlineEditIndicatorBackground, inlineEditIndicatorPrimaryBackground, inlineEditIndicatorPrimaryBorder, inlineEditIndicatorPrimaryForeground, inlineEditIndicatorSecondaryBackground, inlineEditIndicatorSecondaryBorder, inlineEditIndicatorSecondaryForeground, inlineEditIndicatorsuccessfulBackground, inlineEditIndicatorsuccessfulBorder, inlineEditIndicatorsuccessfulForeground } from '../theme.js'; import { mapOutFalsy, rectToProps } from '../utils/utils.js'; import { GutterIndicatorMenuContent } from './gutterIndicatorMenu.js'; -// Represents the user's familiarity with the inline edits feature. -enum UserKind { - FirstTime = 'firstTime', - SecondTime = 'secondTime', - Active = 'active' -} - export class InlineEditsGutterIndicator extends Disposable { private get model() { @@ -48,52 +37,18 @@ export class InlineEditsGutterIndicator extends Disposable { return model; } - private readonly _activeCompletionId = derived(reader => { - const layout = this._layout.read(reader); - if (!layout) { return undefined; } - const model = this._model.read(reader); - if (!model) { return undefined; } - return model.inlineEdit.inlineCompletion.id; - }); - private readonly _gutterIndicatorStyles: IObservable<{ background: string; foreground: string; border: string }>; private readonly _isHoveredOverInlineEditDebounced: IObservable; - private readonly _newUserAnimationDisposable = this._register(new MutableDisposable()); - private readonly _firstToSecondTimeUserDisposable = this._register(new MutableDisposable()); - private readonly _secondTimeToActiveUserDisposable = this._register(new MutableDisposable()); - - private get _newUserType(): UserKind { - return this._storageService.get('inlineEditsGutterIndicatorUserKind', StorageScope.APPLICATION, UserKind.FirstTime) as UserKind; - } - private set _newUserType(value: UserKind) { - switch (value) { - case UserKind.FirstTime: - throw new BugIndicatingError('UserKind should not be set to first time'); - case UserKind.SecondTime: - this._firstToSecondTimeUserDisposable.clear(); - break; - case UserKind.Active: - this._newUserAnimationDisposable.clear(); - this._firstToSecondTimeUserDisposable.clear(); - this._secondTimeToActiveUserDisposable.clear(); - break; - } - - this._storageService.store('inlineEditsGutterIndicatorUserKind', value, StorageScope.APPLICATION, StorageTarget.USER); - } - constructor( private readonly _editorObs: ObservableCodeEditor, private readonly _originalRange: IObservable, private readonly _verticalOffset: IObservable, - private readonly _host: IObservable, private readonly _model: IObservable, private readonly _isHoveringOverInlineEdit: IObservable, private readonly _focusIsInMenu: ISettableObservable, @IHoverService private readonly _hoverService: HoverService, @IInstantiationService private readonly _instantiationService: IInstantiationService, - @IStorageService private readonly _storageService: IStorageService, @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, @IThemeService themeService: IThemeService, ) { @@ -143,14 +98,10 @@ export class InlineEditsGutterIndicator extends Disposable { // pulse animation when hovering inline edit this._register(runOnChange(this._isHoveredOverInlineEditDebounced, (isHovering) => { if (isHovering) { - this._triggerAnimation(); + this.triggerAnimation(); } })); - if (this._newUserType === UserKind.Active) { - this._register(this.setupNewUserExperience()); - } - this._register(autorun(reader => { this._indicator.readEffect(reader); if (this._indicator.element) { @@ -159,87 +110,10 @@ export class InlineEditsGutterIndicator extends Disposable { })); } - private setupNewUserExperience(): IDisposable { - if (this._newUserType === UserKind.Active) { - return Disposable.None; - } - - const disposableStore = new DisposableStore(); - - let userHasHoveredOverIcon = false; - let inlineEditHasBeenAccepted = false; - let firstTimeUserAnimationCount = 0; - let secondTimeUserAnimationCount = 0; - - // pulse animation for new users - disposableStore.add(runOnChange(this._activeCompletionId, async (id) => { - if (id === undefined) { return; } - const userType = this._newUserType; - - // Animation - switch (userType) { - case UserKind.FirstTime: { - for (let i = 0; i < 3 && this._activeCompletionId.get() === id; i++) { - await this._triggerAnimation(); - await timeout(500); - } - break; - } - case UserKind.SecondTime: { - this._triggerAnimation(); - break; - } - } - - // User Kind Transition - switch (userType) { - case UserKind.FirstTime: { - if (++firstTimeUserAnimationCount >= 5 || userHasHoveredOverIcon) { - this._newUserType = UserKind.SecondTime; - } - break; - } - case UserKind.SecondTime: { - if (++secondTimeUserAnimationCount >= 5 && inlineEditHasBeenAccepted) { - this._newUserType = UserKind.Active; - } - break; - } - } - })); - - // Remember when the user has hovered over the icon - disposableStore.add(runOnChange(this._isHoveredOverIconDebounced, async (isHovered) => { - if (isHovered) { - userHasHoveredOverIcon = true; - } - })); - - // Remember when the user has accepted an inline edit - disposableStore.add(autorunWithStore((reader, store) => { - const host = this._host.read(reader); - if (!host) { return; } - store.add(host.onDidAccept(() => { - inlineEditHasBeenAccepted = true; - })); - })); - - return disposableStore; - } - - private _triggerAnimation(): Promise { + public triggerAnimation(): Promise { if (this._accessibilityService.isMotionReduced()) { return new Animation(null, null).finished; } - // WIGGLE ANIMATION: - /* this._iconRef.element.animate([ - { transform: 'rotate(0) scale(1)', offset: 0 }, - { transform: 'rotate(14.4deg) scale(1.1)', offset: 0.15 }, - { transform: 'rotate(-14.4deg) scale(1.2)', offset: 0.3 }, - { transform: 'rotate(14.4deg) scale(1.1)', offset: 0.45 }, - { transform: 'rotate(-14.4deg) scale(1.2)', offset: 0.6 }, - { transform: 'rotate(0) scale(1)', offset: 1 } - ], { duration: 800 }); */ // PULSE ANIMATION: const animation = this._iconRef.element.animate([ @@ -456,11 +330,17 @@ export class InlineEditsGutterIndicator extends Disposable { }; }); + private readonly _iconRef = n.ref(); + + public readonly isVisible = this._layout.map(l => !!l); + private readonly _hoverVisible = observableValue(this, false); public readonly isHoverVisible: IObservable = this._hoverVisible; + private readonly _isHoveredOverIcon = observableValue(this, false); private readonly _isHoveredOverIconDebounced: IObservable = debouncedObservable(this._isHoveredOverIcon, 100); + public readonly isHoveredOverIcon: IObservable = this._isHoveredOverIconDebounced; private _showHover(): void { if (this._hoverVisible.get()) { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsNewUsers.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsNewUsers.ts new file mode 100644 index 000000000000..555d7c565e7c --- /dev/null +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsNewUsers.ts @@ -0,0 +1,184 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { timeout } from '../../../../../../base/common/async.js'; +import { BugIndicatingError } from '../../../../../../base/common/errors.js'; +import { Disposable, DisposableStore, IDisposable, MutableDisposable } from '../../../../../../base/common/lifecycle.js'; +import { autorun, autorunWithStore, derived, IObservable, observableValue, runOnChange, runOnChangeWithCancellationToken } from '../../../../../../base/common/observable.js'; +import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../../../platform/storage/common/storage.js'; +import { InlineEditsGutterIndicator } from './components/gutterIndicatorView.js'; +import { IInlineEditHost, IInlineEditModel } from './inlineEditsViewInterface.js'; +import { InlineEditsCollapsedView } from './inlineEditsViews/inlineEditsCollapsedView.js'; + +enum UserKind { + FirstTime = 'firstTime', + SecondTime = 'secondTime', + Active = 'active' +} + +export class InlineEditsOnboardingExperience extends Disposable { + + private readonly _disposables = this._register(new MutableDisposable()); + + private readonly _setupDone = observableValue({ name: 'setupDone' }, false); + + private readonly _activeCompletionId = derived(reader => { + const model = this._model.read(reader); + if (!model) { return undefined; } + + if (!this._setupDone.read(reader)) { return undefined; } + + const indicator = this._indicator.read(reader); + if (!indicator || !indicator.isVisible.read(reader)) { return undefined; } + + return model.inlineEdit.inlineCompletion.id; + }); + + constructor( + private readonly _host: IObservable, + private readonly _model: IObservable, + private readonly _indicator: IObservable, + private readonly _collapsedView: InlineEditsCollapsedView, + @IStorageService private readonly _storageService: IStorageService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + ) { + super(); + + this._register(this._initializeDebugSetting()); + + // Setup the onboarding experience for new users + if (this.getNewUserType() === UserKind.Active) { // todo: turn === into !== mid month + this._disposables.value = this.setupNewUserExperience(); + } + + // Existing users should not see the onboarding experience when turning on the onbarding experience + this._register(autorunWithStore((reader, store) => { // todo: remove mid month + const host = this._host.read(reader); + if (!host) { return; } + store.add(host.onDidAccept(() => { + this.setNewUserType(UserKind.Active); + })); + })); + + this._setupDone.set(true, undefined); + } + + private setupNewUserExperience(): IDisposable { + if (this.getNewUserType() === UserKind.Active) { + return Disposable.None; + } + + const disposableStore = new DisposableStore(); + + let userHasHoveredOverIcon = false; + let inlineEditHasBeenAccepted = false; + let firstTimeUserAnimationCount = 0; + let secondTimeUserAnimationCount = 0; + + // pulse animation for new users + disposableStore.add(runOnChangeWithCancellationToken(this._activeCompletionId, async (id, _, __, token) => { + if (id === undefined) { return; } + let userType = this.getNewUserType(); + + // User Kind Transition + switch (userType) { + case UserKind.FirstTime: { + if (firstTimeUserAnimationCount++ >= 5 || userHasHoveredOverIcon) { + userType = UserKind.SecondTime; + this.setNewUserType(userType); + } + break; + } + case UserKind.SecondTime: { + if (secondTimeUserAnimationCount++ >= 5 && inlineEditHasBeenAccepted) { + userType = UserKind.Active; + this.setNewUserType(userType); + } + break; + } + } + + // Animation + switch (userType) { + case UserKind.FirstTime: { + for (let i = 0; i < 3 && !token.isCancellationRequested; i++) { + await this._indicator.get()?.triggerAnimation(); + await timeout(500); + } + break; + } + case UserKind.SecondTime: { + this._indicator.get()?.triggerAnimation(); + break; + } + } + })); + + disposableStore.add(autorun(reader => { + if (this._collapsedView.isVisible.read(reader)) { + if (this.getNewUserType() !== UserKind.Active) { + this._collapsedView.triggerAnimation(); + } + } + })); + + // Remember when the user has hovered over the icon + disposableStore.add(autorunWithStore((reader, store) => { + const indicator = this._indicator.read(reader); + if (!indicator) { return; } + store.add(runOnChange(indicator.isHoveredOverIcon, async (isHovered) => { + if (isHovered) { + userHasHoveredOverIcon = true; + } + })); + })); + + // Remember when the user has accepted an inline edit + disposableStore.add(autorunWithStore((reader, store) => { + const host = this._host.read(reader); + if (!host) { return; } + store.add(host.onDidAccept(() => { + inlineEditHasBeenAccepted = true; + })); + })); + + return disposableStore; + } + + private getNewUserType(): UserKind { + return this._storageService.get('inlineEditsGutterIndicatorUserKind', StorageScope.APPLICATION, UserKind.FirstTime) as UserKind; + } + + private setNewUserType(value: UserKind): void { + switch (value) { + case UserKind.FirstTime: + throw new BugIndicatingError('UserKind should not be set to first time'); + case UserKind.SecondTime: + break; + case UserKind.Active: + this._disposables.clear(); + break; + } + + this._storageService.store('inlineEditsGutterIndicatorUserKind', value, StorageScope.APPLICATION, StorageTarget.USER); + } + + private _initializeDebugSetting(): IDisposable { + // Debug setting to reset the new user experience + const hiddenDebugSetting = 'editor.inlineSuggest.edits.resetNewUserExperience'; + if (this._configurationService.getValue(hiddenDebugSetting)) { + this._storageService.remove('inlineEditsGutterIndicatorUserKind', StorageScope.APPLICATION); + } + + const disposable = this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(hiddenDebugSetting) && this._configurationService.getValue(hiddenDebugSetting)) { + this._storageService.remove('inlineEditsGutterIndicatorUserKind', StorageScope.APPLICATION); + } + }); + + return disposable; + } +} 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 522c4df304ee..fbf0fb2faefd 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsView.ts @@ -22,6 +22,7 @@ import { TextModel } from '../../../../../common/model/textModel.js'; import { InlineEditsGutterIndicator } from './components/gutterIndicatorView.js'; import { InlineEditWithChanges } from './inlineEditWithChanges.js'; import { GhostTextIndicator, InlineEditHost, InlineEditModel } from './inlineEditsModel.js'; +import { InlineEditsOnboardingExperience } from './inlineEditsNewUsers.js'; import { IInlineEditModel, InlineEditTabAction } from './inlineEditsViewInterface.js'; import { InlineEditsCollapsedView } from './inlineEditsViews/inlineEditsCollapsedView.js'; import { InlineEditsDeletionView } from './inlineEditsViews/inlineEditsDeletionView.js'; @@ -90,6 +91,9 @@ export class InlineEditsView extends Disposable { this._wordReplacementViews.recomputeInitiallyAndOnChange(this._store); this._indicatorCyclicDependencyCircuitBreaker.set(true, undefined); + + this._register(this._instantiationService.createInstance(InlineEditsOnboardingExperience, this._host, this._model, this._indicator, this._inlineCollapsedView)); + this._constructorDone.set(true, undefined); // TODO: remove and use correct initialization order } @@ -204,7 +208,6 @@ export class InlineEditsView extends Disposable { this._editorObs, indicatorDisplayRange, this._gutterIndicatorOffset, - this._host, modelWithGhostTextSupport, this._inlineEditsIsHovered, this._focusIsInMenu, diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViewInterface.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViewInterface.ts index ec950b38e5b8..a1b85ce2c02e 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViewInterface.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViewInterface.ts @@ -21,6 +21,7 @@ export interface IInlineEditsView { } export interface IInlineEditHost { + readonly onDidAccept: Event; inAcceptFlow: IObservable; } 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 2acbdd9d729e..f81f5278a505 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 @@ -7,6 +7,7 @@ import { IMouseEvent } from '../../../../../../../base/browser/mouseEvent.js'; import { Emitter } from '../../../../../../../base/common/event.js'; import { Disposable } from '../../../../../../../base/common/lifecycle.js'; import { constObservable, derived, IObservable } from '../../../../../../../base/common/observable.js'; +import { IAccessibilityService } from '../../../../../../../platform/accessibility/common/accessibility.js'; import { asCssVariable } from '../../../../../../../platform/theme/common/colorUtils.js'; import { ICodeEditor } from '../../../../../../browser/editorBrowser.js'; import { ObservableCodeEditor, observableCodeEditor } from '../../../../../../browser/observableCodeEditor.js'; @@ -23,10 +24,14 @@ export class InlineEditsCollapsedView extends Disposable implements IInlineEdits readonly onDidClick = this._onDidClick.event; private readonly _editorObs: ObservableCodeEditor; + private readonly _iconRef = n.ref(); + + readonly isVisible: IObservable; constructor( private readonly _editor: ICodeEditor, private readonly _edit: IObservable, + @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, ) { super(); @@ -65,6 +70,27 @@ export class InlineEditsCollapsedView extends Disposable implements IInlineEdits allowEditorOverflow: false, minContentWidthInPx: constObservable(0), })); + + this.isVisible = this._edit.map((inlineEdit, reader) => !!inlineEdit && startPoint.read(reader) !== null); + } + + public triggerAnimation(): Promise { + if (this._accessibilityService.isMotionReduced()) { + return new Animation(null, null).finished; + } + + // PULSE ANIMATION: + const animation = this._iconRef.element.animate([ + { offset: 0.00, transform: 'translateY(-3px)', }, + { offset: 0.20, transform: 'translateY(1px)', }, + { offset: 0.36, transform: 'translateY(-1px)', }, + { offset: 0.52, transform: 'translateY(1px)', }, + { offset: 0.68, transform: 'translateY(-1px)', }, + { offset: 0.84, transform: 'translateY(1px)', }, + { offset: 1.00, transform: 'translateY(0px)', }, + ], { duration: 2000 }); + + return animation.finished; } private getCollapsedIndicator(startPoint: IObservable) { @@ -74,6 +100,7 @@ export class InlineEditsCollapsedView extends Disposable implements IInlineEdits return n.svg({ class: 'collapsedView', + ref: this._iconRef, style: { position: 'absolute', top: 0, diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/theme.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/theme.ts index 656469803f8e..ff04b209adc8 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/theme.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/theme.ts @@ -5,11 +5,10 @@ import { Color } from '../../../../../../base/common/color.js'; import { BugIndicatingError } from '../../../../../../base/common/errors.js'; -import { IObservable } from '../../../../../../base/common/observable.js'; -import { observableFromEventOpts } from '../../../../../../base/common/observableInternal/utils.js'; +import { IObservable, observableFromEventOpts } from '../../../../../../base/common/observable.js'; import { localize } from '../../../../../../nls.js'; -import { diffRemoved, diffInsertedLine, diffInserted, buttonBackground, buttonForeground, buttonSecondaryBackground, buttonSecondaryForeground, editorBackground } from '../../../../../../platform/theme/common/colorRegistry.js'; -import { registerColor, transparent, darken, ColorIdentifier } from '../../../../../../platform/theme/common/colorUtils.js'; +import { buttonBackground, buttonForeground, buttonSecondaryBackground, buttonSecondaryForeground, diffInserted, diffInsertedLine, diffRemoved, editorBackground } from '../../../../../../platform/theme/common/colorRegistry.js'; +import { ColorIdentifier, darken, registerColor, transparent } from '../../../../../../platform/theme/common/colorUtils.js'; import { IThemeService } from '../../../../../../platform/theme/common/themeService.js'; import { InlineEditTabAction } from './inlineEditsViewInterface.js'; diff --git a/src/vs/editor/contrib/rename/browser/rename.ts b/src/vs/editor/contrib/rename/browser/rename.ts index 2165ff1e350e..4fda88fd913e 100644 --- a/src/vs/editor/contrib/rename/browser/rename.ts +++ b/src/vs/editor/contrib/rename/browser/rename.ts @@ -12,6 +12,16 @@ import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; import { DisposableStore } from '../../../../base/common/lifecycle.js'; import { assertType } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; +import * as nls from '../../../../nls.js'; +import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js'; +import { ConfigurationScope, Extensions, IConfigurationRegistry } from '../../../../platform/configuration/common/configurationRegistry.js'; +import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; +import { INotificationService } from '../../../../platform/notification/common/notification.js'; +import { IEditorProgressService } from '../../../../platform/progress/common/progress.js'; +import { Registry } from '../../../../platform/registry/common/platform.js'; import { ICodeEditor } from '../../../browser/editorBrowser.js'; import { EditorAction, EditorCommand, EditorContributionInstantiation, ServicesAccessor, registerEditorAction, registerEditorCommand, registerEditorContribution, registerModelAndPositionCommand } from '../../../browser/editorExtensions.js'; import { IBulkEditService } from '../../../browser/services/bulkEditService.js'; @@ -27,18 +37,7 @@ import { ILanguageFeaturesService } from '../../../common/services/languageFeatu import { ITextResourceConfigurationService } from '../../../common/services/textResourceConfiguration.js'; import { CodeEditorStateFlag, EditorStateCancellationTokenSource } from '../../editorState/browser/editorState.js'; import { MessageController } from '../../message/browser/messageController.js'; -import * as nls from '../../../../nls.js'; -import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js'; -import { ConfigurationScope, Extensions, IConfigurationRegistry } from '../../../../platform/configuration/common/configurationRegistry.js'; -import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; -import { ILogService } from '../../../../platform/log/common/log.js'; -import { INotificationService } from '../../../../platform/notification/common/notification.js'; -import { IEditorProgressService } from '../../../../platform/progress/common/progress.js'; -import { Registry } from '../../../../platform/registry/common/platform.js'; -import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; -import { CONTEXT_RENAME_INPUT_VISIBLE, NewNameSource, RenameWidget, RenameWidgetResult } from './renameWidget.js'; +import { CONTEXT_RENAME_INPUT_VISIBLE, RenameWidget } from './renameWidget.js'; class RenameSkeleton { @@ -151,7 +150,6 @@ class RenameController implements IEditorContribution { @ILogService private readonly _logService: ILogService, @ITextResourceConfigurationService private readonly _configService: ITextResourceConfigurationService, @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, - @ITelemetryService private readonly _telemetryService: ITelemetryService, ) { this._renameWidget = this._disposableStore.add(this._instaService.createInstance(RenameWidget, this.editor, ['acceptRenameInput', 'acceptRenameInputWithPreview'])); } @@ -254,10 +252,6 @@ class RenameController implements IEditorContribution { ); trace('received response from rename input field'); - if (newSymbolNamesProviders.length > 0) { // @ulugbekna: we're interested only in telemetry for rename suggestions currently - this._reportTelemetry(newSymbolNamesProviders.length, model.getLanguageId(), inputFieldResult); - } - // no result, only hint to focus the editor or not if (typeof inputFieldResult === 'boolean') { trace(`returning early - rename input field response - ${inputFieldResult}`); @@ -343,66 +337,6 @@ class RenameController implements IEditorContribution { focusPreviousRenameSuggestion(): void { this._renameWidget.focusPreviousRenameSuggestion(); } - - private _reportTelemetry(nRenameSuggestionProviders: number, languageId: string, inputFieldResult: boolean | RenameWidgetResult) { - type RenameInvokedEvent = - { - kind: 'accepted' | 'cancelled'; - languageId: string; - nRenameSuggestionProviders: number; - - /** provided only if kind = 'accepted' */ - source?: NewNameSource['k']; - /** provided only if kind = 'accepted' */ - nRenameSuggestions?: number; - /** provided only if kind = 'accepted' */ - timeBeforeFirstInputFieldEdit?: number; - /** provided only if kind = 'accepted' */ - wantsPreview?: boolean; - /** provided only if kind = 'accepted' */ - nRenameSuggestionsInvocations?: number; - /** provided only if kind = 'accepted' */ - hadAutomaticRenameSuggestionsInvocation?: boolean; - }; - - type RenameInvokedClassification = { - owner: 'ulugbekna'; - comment: 'A rename operation was invoked.'; - - kind: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the rename operation was cancelled or accepted.' }; - languageId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Document language ID.' }; - nRenameSuggestionProviders: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of rename providers for this document.' }; - - source?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the new name came from the input field or rename suggestions.' }; - nRenameSuggestions?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of rename suggestions user has got' }; - timeBeforeFirstInputFieldEdit?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Milliseconds before user edits the input field for the first time' }; - wantsPreview?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'If user wanted preview.' }; - nRenameSuggestionsInvocations?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of times rename suggestions were invoked' }; - hadAutomaticRenameSuggestionsInvocation?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether rename suggestions were invoked automatically' }; - }; - - const value: RenameInvokedEvent = - typeof inputFieldResult === 'boolean' - ? { - kind: 'cancelled', - languageId, - nRenameSuggestionProviders, - } - : { - kind: 'accepted', - languageId, - nRenameSuggestionProviders, - - source: inputFieldResult.stats.source.k, - nRenameSuggestions: inputFieldResult.stats.nRenameSuggestions, - timeBeforeFirstInputFieldEdit: inputFieldResult.stats.timeBeforeFirstInputFieldEdit, - wantsPreview: inputFieldResult.wantsPreview, - nRenameSuggestionsInvocations: inputFieldResult.stats.nRenameSuggestionsInvocations, - hadAutomaticRenameSuggestionsInvocation: inputFieldResult.stats.hadAutomaticRenameSuggestionsInvocation, - }; - - this._telemetryService.publicLog2('renameInvokedEvent', value); - } } // ---- action implementation diff --git a/src/vs/editor/standalone/browser/quickInput/standaloneQuickInputService.ts b/src/vs/editor/standalone/browser/quickInput/standaloneQuickInputService.ts index 03a2821ce51b..0b0da3e8e377 100644 --- a/src/vs/editor/standalone/browser/quickInput/standaloneQuickInputService.ts +++ b/src/vs/editor/standalone/browser/quickInput/standaloneQuickInputService.ts @@ -174,9 +174,11 @@ export class QuickInputEditorContribution implements IEditorContribution { return editor.getContribution(QuickInputEditorContribution.ID); } - readonly widget = new QuickInputEditorWidget(this.editor); + readonly widget: QuickInputEditorWidget; - constructor(private editor: ICodeEditor) { } + constructor(private editor: ICodeEditor) { + this.widget = new QuickInputEditorWidget(this.editor); + } dispose(): void { this.widget.dispose(); diff --git a/src/vs/platform/actionWidget/browser/actionList.ts b/src/vs/platform/actionWidget/browser/actionList.ts index bebab4ec9a4b..ff973d299fec 100644 --- a/src/vs/platform/actionWidget/browser/actionList.ts +++ b/src/vs/platform/actionWidget/browser/actionList.ts @@ -18,6 +18,7 @@ import { IContextViewService } from '../../contextview/browser/contextView.js'; import { IKeybindingService } from '../../keybinding/common/keybinding.js'; import { defaultListStyles } from '../../theme/browser/defaultStyles.js'; import { asCssVariable } from '../../theme/common/colorRegistry.js'; +import { ILayoutService } from '../../layout/browser/layoutService.js'; export const acceptSelectedActionCommand = 'acceptSelectedCodeAction'; export const previewSelectedActionCommand = 'previewSelectedCodeAction'; @@ -182,10 +183,10 @@ export class ActionList extends Disposable { items: readonly IActionListItem[], private readonly _delegate: IActionListDelegate, @IContextViewService private readonly _contextViewService: IContextViewService, - @IKeybindingService private readonly _keybindingService: IKeybindingService + @IKeybindingService private readonly _keybindingService: IKeybindingService, + @ILayoutService private readonly _layoutService: ILayoutService, ) { super(); - this.domNode = document.createElement('div'); this.domNode.classList.add('actionList'); const virtualDelegate: IListVirtualDelegate> = { @@ -270,7 +271,7 @@ export class ActionList extends Disposable { } const maxVhPrecentage = 0.7; - const height = Math.min(heightWithHeaders, this.domNode.ownerDocument.body.clientHeight * maxVhPrecentage); + const height = Math.min(heightWithHeaders, this._layoutService.getContainer(dom.getWindow(this.domNode)).clientHeight * maxVhPrecentage); this._list.layout(height, maxWidth); this.domNode.style.height = `${height}px`; diff --git a/src/vs/platform/configuration/common/configurations.ts b/src/vs/platform/configuration/common/configurations.ts index b3749c2f709f..2dea3c6ce989 100644 --- a/src/vs/platform/configuration/common/configurations.ts +++ b/src/vs/platform/configuration/common/configurations.ts @@ -23,13 +23,14 @@ export class DefaultConfiguration extends Disposable { private readonly _onDidChangeConfiguration = this._register(new Emitter<{ defaults: ConfigurationModel; properties: string[] }>()); readonly onDidChangeConfiguration = this._onDidChangeConfiguration.event; - private _configurationModel = ConfigurationModel.createEmptyModel(this.logService); + private _configurationModel: ConfigurationModel; get configurationModel(): ConfigurationModel { return this._configurationModel; } constructor(private readonly logService: ILogService) { super(); + this._configurationModel = ConfigurationModel.createEmptyModel(logService); } async initialize(): Promise { @@ -94,7 +95,7 @@ export class PolicyConfiguration extends Disposable implements IPolicyConfigurat private readonly configurationRegistry: IConfigurationRegistry; - private _configurationModel = ConfigurationModel.createEmptyModel(this.logService); + private _configurationModel: ConfigurationModel; get configurationModel() { return this._configurationModel; } constructor( @@ -103,6 +104,7 @@ export class PolicyConfiguration extends Disposable implements IPolicyConfigurat @ILogService private readonly logService: ILogService ) { super(); + this._configurationModel = ConfigurationModel.createEmptyModel(this.logService); this.configurationRegistry = Registry.as(Extensions.Configuration); } diff --git a/src/vs/platform/dialogs/common/dialogs.ts b/src/vs/platform/dialogs/common/dialogs.ts index bb9270b9c287..b0420f5c5a87 100644 --- a/src/vs/platform/dialogs/common/dialogs.ts +++ b/src/vs/platform/dialogs/common/dialogs.ts @@ -283,7 +283,8 @@ export interface ICustomDialogOptions { export interface ICustomDialogMarkdown { readonly markdown: IMarkdownString; readonly classes?: string[]; - readonly dismissOnLinkClick?: boolean; + /** Custom link handler for markdown content, see {@link IContentActionHandler}. Defaults to {@link openLinkFromMarkdown}. */ + actionHandler?(link: string): Promise; } /** diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index a0afbe7f79fa..2d5de5f3aba0 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -30,6 +30,7 @@ import { StopWatch } from '../../../base/common/stopwatch.js'; import { format2 } from '../../../base/common/strings.js'; import { IAssignmentService } from '../../assignment/common/assignment.js'; import { ExtensionGalleryResourceType, Flag, getExtensionGalleryManifestResourceUri, IExtensionGalleryManifest, IExtensionGalleryManifestService } from './extensionGalleryManifest.js'; +import { TelemetryTrustedValue } from '../../telemetry/common/telemetryUtils.js'; const CURRENT_TARGET_PLATFORM = isWeb ? TargetPlatform.WEB : getTargetPlatform(platform, arch); const SEARCH_ACTIVITY_HEADER_NAME = 'X-Market-Search-Activity-Id'; @@ -198,9 +199,9 @@ type GalleryServiceQueryEvent = QueryTelemetryData & { readonly statusCode?: string; readonly errorCode?: string; readonly count?: string; - readonly server?: string; - readonly endToEndId?: string; - readonly activityId?: string; + readonly server?: TelemetryTrustedValue; + readonly endToEndId?: TelemetryTrustedValue; + readonly activityId?: TelemetryTrustedValue; }; type GalleryServiceAdditionalQueryClassification = { @@ -1379,9 +1380,10 @@ export abstract class AbstractExtensionGalleryService implements IExtensionGalle } } - private getHeaderValue(headers: IHeaders | undefined, name: string): string | undefined { - const value = headers?.[name.toLowerCase()]; - return Array.isArray(value) ? value[0] : value; + private getHeaderValue(headers: IHeaders | undefined, name: string): TelemetryTrustedValue | undefined { + const headerValue = headers?.[name.toLowerCase()]; + const value = Array.isArray(headerValue) ? headerValue[0] : headerValue; + return value ? new TelemetryTrustedValue(value) : undefined; } private async getLatestRawGalleryExtension(extension: string, uri: URI, token: CancellationToken): Promise { @@ -1453,9 +1455,9 @@ export abstract class AbstractExtensionGalleryService implements IExtensionGalle host: string; duration: number; errorCode?: string; - server?: string; - activityId?: string; - endToEndId?: string; + server?: TelemetryTrustedValue; + activityId?: TelemetryTrustedValue; + endToEndId?: TelemetryTrustedValue; }; this.telemetryService.publicLog2('galleryService:getLatest', { extension, @@ -1711,9 +1713,9 @@ export abstract class AbstractExtensionGalleryService implements IExtensionGalle assetType: string; message: string; extensionVersion: string; - server?: string; - endToEndId?: string; - activityId?: string; + server?: TelemetryTrustedValue; + endToEndId?: TelemetryTrustedValue; + activityId?: TelemetryTrustedValue; }; this.telemetryService.publicLog2('galleryService:cdnFallback', { extension, diff --git a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts index 2186e8d1d107..a15e92a42ab7 100644 --- a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts +++ b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts @@ -155,15 +155,15 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem private readonly _onDidChangeCache = this._register(new Emitter()); readonly onDidChangeCache = this._onDidChangeCache.event; - private readonly systemExtensionsCachedScanner = this._register(this.instantiationService.createInstance(CachedExtensionsScanner, this.currentProfile)); - private readonly userExtensionsCachedScanner = this._register(this.instantiationService.createInstance(CachedExtensionsScanner, this.currentProfile)); - private readonly extensionsScanner = this._register(this.instantiationService.createInstance(ExtensionsScanner)); + private readonly systemExtensionsCachedScanner: CachedExtensionsScanner; + private readonly userExtensionsCachedScanner: CachedExtensionsScanner; + private readonly extensionsScanner: ExtensionsScanner; constructor( readonly systemExtensionsLocation: URI, readonly userExtensionsLocation: URI, private readonly extensionsControlLocation: URI, - private readonly currentProfile: IUserDataProfile, + currentProfile: IUserDataProfile, @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, @IExtensionsProfileScannerService protected readonly extensionsProfileScannerService: IExtensionsProfileScannerService, @IFileService protected readonly fileService: IFileService, @@ -175,6 +175,10 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem ) { super(); + this.systemExtensionsCachedScanner = this._register(this.instantiationService.createInstance(CachedExtensionsScanner, currentProfile)); + this.userExtensionsCachedScanner = this._register(this.instantiationService.createInstance(CachedExtensionsScanner, currentProfile)); + this.extensionsScanner = this._register(this.instantiationService.createInstance(ExtensionsScanner)); + this._register(this.systemExtensionsCachedScanner.onDidChangeCache(() => this._onDidChangeCache.fire(ExtensionType.System))); this._register(this.userExtensionsCachedScanner.onDidChangeCache(() => this._onDidChangeCache.fire(ExtensionType.User))); } diff --git a/src/vs/platform/extensions/common/extensions.ts b/src/vs/platform/extensions/common/extensions.ts index b38ba82a1d9a..8e68cbe06d1f 100644 --- a/src/vs/platform/extensions/common/extensions.ts +++ b/src/vs/platform/extensions/common/extensions.ts @@ -280,6 +280,7 @@ export interface IRelaxedExtensionManifest { engines: { readonly vscode: string }; description?: string; main?: string; + type?: string; browser?: string; preview?: boolean; // For now this only supports pointing to l10n bundle files diff --git a/src/vs/platform/native/common/native.ts b/src/vs/platform/native/common/native.ts index 4d60c1d9f1bb..e27548fc2cea 100644 --- a/src/vs/platform/native/common/native.ts +++ b/src/vs/platform/native/common/native.ts @@ -143,7 +143,7 @@ export interface ICommonNativeHostService { hasWSLFeatureInstalled(): Promise; // Screenshots - getScreenshot(): Promise; + getScreenshot(): Promise; // Process getProcessId(): Promise; diff --git a/src/vs/platform/native/electron-main/nativeHostMainService.ts b/src/vs/platform/native/electron-main/nativeHostMainService.ts index 6324e98e290f..5793ec0262c6 100644 --- a/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -720,11 +720,12 @@ export class NativeHostMainService extends Disposable implements INativeHostMain //#region Screenshots - async getScreenshot(windowId: number | undefined, options?: INativeHostOptions): Promise { + async getScreenshot(windowId: number | undefined, options?: INativeHostOptions): Promise { const window = this.windowById(options?.targetWindowId, windowId); const captured = await window?.win?.webContents.capturePage(); - return captured?.toJPEG(95); + const buf = captured?.toJPEG(95); + return buf && VSBuffer.wrap(buf); } //#endregion diff --git a/src/vs/platform/observable/common/observableMemento.ts b/src/vs/platform/observable/common/observableMemento.ts index cf38e2fb80b2..f901636f2b2f 100644 --- a/src/vs/platform/observable/common/observableMemento.ts +++ b/src/vs/platform/observable/common/observableMemento.ts @@ -5,7 +5,9 @@ import { strictEquals } from '../../../base/common/equals.js'; import { DisposableStore, IDisposable } from '../../../base/common/lifecycle.js'; +// eslint-disable-next-line local/code-no-deep-import-of-internal import { ObservableValue } from '../../../base/common/observableInternal/base.js'; +// eslint-disable-next-line local/code-no-deep-import-of-internal import { DebugNameData } from '../../../base/common/observableInternal/debugName.js'; import { IStorageService, StorageScope, StorageTarget } from '../../storage/common/storage.js'; diff --git a/src/vs/platform/policy/common/policy.ts b/src/vs/platform/policy/common/policy.ts index f2d6ab803f79..80b042416b05 100644 --- a/src/vs/platform/policy/common/policy.ts +++ b/src/vs/platform/policy/common/policy.ts @@ -39,7 +39,7 @@ export abstract class AbstractPolicyService extends Disposable implements IPolic this.policyDefinitions = { ...policyDefinitions, ...this.policyDefinitions }; if (size !== Object.keys(this.policyDefinitions).length) { - await this._updatePolicyDefinitions(policyDefinitions); + await this._updatePolicyDefinitions(this.policyDefinitions); } return Iterable.reduce(this.policies.entries(), (r, [name, value]) => ({ ...r, [name]: value }), {}); diff --git a/src/vs/platform/quickinput/browser/commandsQuickAccess.ts b/src/vs/platform/quickinput/browser/commandsQuickAccess.ts index 513641b4da51..d2c32d037745 100644 --- a/src/vs/platform/quickinput/browser/commandsQuickAccess.ts +++ b/src/vs/platform/quickinput/browser/commandsQuickAccess.ts @@ -49,13 +49,13 @@ export abstract class AbstractCommandsQuickAccessProvider extends PickerQuickAcc private static WORD_FILTER = or(matchesPrefix, matchesWords, matchesContiguousSubString); - private readonly commandsHistory = this._register(this.instantiationService.createInstance(CommandsHistory)); + private readonly commandsHistory: CommandsHistory; protected override readonly options: ICommandsQuickAccessOptions; constructor( options: ICommandsQuickAccessOptions, - @IInstantiationService private readonly instantiationService: IInstantiationService, + @IInstantiationService instantiationService: IInstantiationService, @IKeybindingService protected readonly keybindingService: IKeybindingService, @ICommandService private readonly commandService: ICommandService, @ITelemetryService private readonly telemetryService: ITelemetryService, @@ -63,6 +63,8 @@ export abstract class AbstractCommandsQuickAccessProvider extends PickerQuickAcc ) { super(AbstractCommandsQuickAccessProvider.PREFIX, options); + this.commandsHistory = this._register(instantiationService.createInstance(CommandsHistory)); + this.options = options; } diff --git a/src/vs/platform/terminal/common/capabilities/capabilities.ts b/src/vs/platform/terminal/common/capabilities/capabilities.ts index 17b2429ff8fa..268c20a0d773 100644 --- a/src/vs/platform/terminal/common/capabilities/capabilities.ts +++ b/src/vs/platform/terminal/common/capabilities/capabilities.ts @@ -215,6 +215,7 @@ export interface ICommandDetectionCapability { /** The current cwd at the cursor's position. */ readonly cwd: string | undefined; readonly hasRichCommandDetection: boolean; + readonly promptType: string | undefined; readonly currentCommand: ICurrentPartialCommand | undefined; readonly onCommandStarted: Event; readonly onCommandFinished: Event; @@ -242,6 +243,7 @@ export interface ICommandDetectionCapability { handleCommandExecuted(options?: IHandleCommandOptions): void; handleCommandFinished(exitCode?: number, options?: IHandleCommandOptions): void; setHasRichCommandDetection(value: boolean): void; + setPromptType(value: string): void; /** * Set the command line explicitly. * @param commandLine The command line being set. diff --git a/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts b/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts index 41efe327cfa3..c0bc3c75943e 100644 --- a/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts +++ b/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts @@ -35,6 +35,8 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe private _handleCommandStartOptions?: IHandleCommandOptions; private _hasRichCommandDetection: boolean = false; get hasRichCommandDetection() { return this._hasRichCommandDetection; } + private _promptType: string | undefined; + get promptType(): string | undefined { return this._promptType; } private _ptyHeuristicsHooks: ICommandDetectionHeuristicsHooks; private readonly _ptyHeuristics: MandatoryMutableDisposable; @@ -232,6 +234,10 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe this._onSetRichCommandDetection.fire(value); } + setPromptType(value: string): void { + this._promptType = value; + } + setIsCommandStorageDisabled(): void { this.__isCommandStorageDisabled = true; } diff --git a/src/vs/platform/terminal/common/terminal.ts b/src/vs/platform/terminal/common/terminal.ts index 3f326233dc79..9065497b3fa0 100644 --- a/src/vs/platform/terminal/common/terminal.ts +++ b/src/vs/platform/terminal/common/terminal.ts @@ -251,7 +251,8 @@ export const enum ProcessPropertyType { ResolvedShellLaunchConfig = 'resolvedShellLaunchConfig', OverrideDimensions = 'overrideDimensions', FailedShellIntegrationActivation = 'failedShellIntegrationActivation', - UsedShellIntegrationInjection = 'usedShellIntegrationInjection' + UsedShellIntegrationInjection = 'usedShellIntegrationInjection', + ShellIntegrationInjectionFailureReason = 'shellIntegrationInjectionFailureReason', } export interface IProcessProperty { @@ -270,6 +271,7 @@ export interface IProcessPropertyMap { [ProcessPropertyType.OverrideDimensions]: ITerminalDimensionsOverride | undefined; [ProcessPropertyType.FailedShellIntegrationActivation]: boolean | undefined; [ProcessPropertyType.UsedShellIntegrationInjection]: boolean | undefined; + [ProcessPropertyType.ShellIntegrationInjectionFailureReason]: ShellIntegrationInjectionFailureReason | undefined; } export interface IFixedTerminalDimensions { @@ -975,6 +977,40 @@ export const enum ShellIntegrationStatus { VSCode } + +export const enum ShellIntegrationInjectionFailureReason { + /** + * The setting is disabled. + */ + InjectionSettingDisabled = 'injectionSettingDisabled', + /** + * There is no executable (so there's no way to determine how to inject). + */ + NoExecutable = 'noExecutable', + /** + * It's a feature terminal (tasks, debug), unless it's explicitly being forced. + */ + FeatureTerminal = 'featureTerminal', + /** + * The ignoreShellIntegration flag is passed (eg. relaunching without shell integration). + */ + IgnoreShellIntegrationFlag = 'ignoreShellIntegrationFlag', + /** + * Shell integration doesn't work with winpty. + */ + Winpty = 'winpty', + /** + * We're conservative whether we inject when we don't recognize the arguments used for the + * shell as we would prefer launching one without shell integration than breaking their profile. + */ + UnsupportedArgs = 'unsupportedArgs', + /** + * The shell doesn't have built-in shell integration. Note that this doesn't mean the shell + * won't have shell integration in the end. + */ + UnsupportedShell = 'unsupportedShell', +} + export enum TerminalExitReason { Unknown = 0, Shutdown = 1, diff --git a/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts b/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts index f17dbb5be08e..49ff56819b12 100644 --- a/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts +++ b/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts @@ -583,6 +583,10 @@ export class ShellIntegrationAddon extends Disposable implements IShellIntegrati this._updatePromptTerminator(sanitizedValue); return true; } + case 'PromptType': { + this._createOrGetCommandDetection(this._terminal).setPromptType(value); + return true; + } case 'Task': { this._createOrGetBufferMarkDetection(this._terminal); this.capabilities.get(TerminalCapability.CommandDetection)?.setIsCommandStorageDisabled(); diff --git a/src/vs/platform/terminal/node/terminalEnvironment.ts b/src/vs/platform/terminal/node/terminalEnvironment.ts index a48d1b84975a..ebe103902740 100644 --- a/src/vs/platform/terminal/node/terminalEnvironment.ts +++ b/src/vs/platform/terminal/node/terminalEnvironment.ts @@ -11,7 +11,7 @@ import * as process from '../../../base/common/process.js'; import { format } from '../../../base/common/strings.js'; import { ILogService } from '../../log/common/log.js'; import { IProductService } from '../../product/common/productService.js'; -import { IShellLaunchConfig, ITerminalEnvironment, ITerminalProcessOptions } from '../common/terminal.js'; +import { IShellLaunchConfig, ITerminalEnvironment, ITerminalProcessOptions, ShellIntegrationInjectionFailureReason } from '../common/terminal.js'; import { EnvironmentVariableMutatorType } from '../common/environmentVariable.js'; import { deserializeEnvironmentVariableCollections } from '../common/environmentVariableShared.js'; import { MergedEnvironmentVariableCollection } from '../common/environmentVariableCollection.js'; @@ -26,23 +26,29 @@ export function getWindowsBuildNumber(): number { } export interface IShellIntegrationConfigInjection { + readonly type: 'injection'; /** * A new set of arguments to use. */ - newArgs: string[] | undefined; + readonly newArgs: string[] | undefined; /** * An optional environment to mixing to the real environment. */ - envMixin?: IProcessEnvironment; + readonly envMixin?: IProcessEnvironment; /** * An optional array of files to copy from `source` to `dest`. */ - filesToCopy?: { + readonly filesToCopy?: { source: string; dest: string; }[]; } +export interface IShellIntegrationInjectionFailure { + readonly type: 'failure'; + readonly reason: ShellIntegrationInjectionFailureReason; +} + /** * For a given shell launch config, returns arguments to replace and an optional environment to * mixin to the SLC's environment to enable shell integration. This must be run within the context @@ -55,30 +61,32 @@ export function getShellIntegrationInjection( env: ITerminalEnvironment | undefined, logService: ILogService, productService: IProductService -): IShellIntegrationConfigInjection | undefined { - // Conditionally disable shell integration arg injection - // - The global setting is disabled - // - There is no executable (not sure what script to run) - // - The terminal is used by a feature like tasks or debugging - const useWinpty = isWindows && (!options.windowsEnableConpty || getWindowsBuildNumber() < 18309); - if ( - // The global setting is disabled - !options.shellIntegration.enabled || - // There is no executable (so there's no way to determine how to inject) - !shellLaunchConfig.executable || - // It's a feature terminal (tasks, debug), unless it's explicitly being forced - (shellLaunchConfig.isFeatureTerminal && !shellLaunchConfig.forceShellIntegration) || - // The ignoreShellIntegration flag is passed (eg. relaunching without shell integration) - shellLaunchConfig.ignoreShellIntegration || - // Winpty is unsupported - useWinpty - ) { - return undefined; +): IShellIntegrationConfigInjection | IShellIntegrationInjectionFailure { + // The global setting is disabled + if (!options.shellIntegration.enabled) { + return { type: 'failure', reason: ShellIntegrationInjectionFailureReason.InjectionSettingDisabled }; + } + // There is no executable (so there's no way to determine how to inject) + if (!shellLaunchConfig.executable) { + return { type: 'failure', reason: ShellIntegrationInjectionFailureReason.NoExecutable }; + } + // It's a feature terminal (tasks, debug), unless it's explicitly being forced + if (shellLaunchConfig.isFeatureTerminal && !shellLaunchConfig.forceShellIntegration) { + return { type: 'failure', reason: ShellIntegrationInjectionFailureReason.FeatureTerminal }; + } + // The ignoreShellIntegration flag is passed (eg. relaunching without shell integration) + if (shellLaunchConfig.ignoreShellIntegration) { + return { type: 'failure', reason: ShellIntegrationInjectionFailureReason.IgnoreShellIntegrationFlag }; + } + // Shell integration doesn't work with winpty + if (isWindows && (!options.windowsEnableConpty || getWindowsBuildNumber() < 18309)) { + return { type: 'failure', reason: ShellIntegrationInjectionFailureReason.Winpty }; } const originalArgs = shellLaunchConfig.args; const shell = process.platform === 'win32' ? path.basename(shellLaunchConfig.executable).toLowerCase() : path.basename(shellLaunchConfig.executable); const appRoot = path.dirname(FileAccess.asFileUri('').fsPath); + const type = 'injection'; let newArgs: string[] | undefined; const envMixin: IProcessEnvironment = { 'VSCODE_INJECTION': '1' @@ -106,7 +114,7 @@ export function getShellIntegrationInjection( newArgs = shellIntegrationArgs.get(ShellIntegrationExecutable.WindowsPwshLogin); } if (!newArgs) { - return undefined; + return { type: 'failure', reason: ShellIntegrationInjectionFailureReason.UnsupportedArgs }; } newArgs = [...newArgs]; // Shallow clone the array to avoid setting the default array newArgs[newArgs.length - 1] = format(newArgs[newArgs.length - 1], appRoot, ''); @@ -114,7 +122,7 @@ export function getShellIntegrationInjection( if (options.shellIntegration.suggestEnabled) { envMixin['VSCODE_SUGGEST'] = '1'; } - return { newArgs, envMixin }; + return { type, newArgs, envMixin }; } else if (shell === 'bash.exe') { if (!originalArgs || originalArgs.length === 0) { newArgs = shellIntegrationArgs.get(ShellIntegrationExecutable.Bash); @@ -124,15 +132,15 @@ export function getShellIntegrationInjection( newArgs = shellIntegrationArgs.get(ShellIntegrationExecutable.Bash); } if (!newArgs) { - return undefined; + return { type: 'failure', reason: ShellIntegrationInjectionFailureReason.UnsupportedArgs }; } newArgs = [...newArgs]; // Shallow clone the array to avoid setting the default array newArgs[newArgs.length - 1] = format(newArgs[newArgs.length - 1], appRoot); envMixin['VSCODE_STABLE'] = productService.quality === 'stable' ? '1' : '0'; - return { newArgs, envMixin }; + return { type, newArgs, envMixin }; } logService.warn(`Shell integration cannot be enabled for executable "${shellLaunchConfig.executable}" and args`, shellLaunchConfig.args); - return undefined; + return { type: 'failure', reason: ShellIntegrationInjectionFailureReason.UnsupportedShell }; } // Linux & macOS @@ -146,12 +154,12 @@ export function getShellIntegrationInjection( newArgs = shellIntegrationArgs.get(ShellIntegrationExecutable.Bash); } if (!newArgs) { - return undefined; + return { type: 'failure', reason: ShellIntegrationInjectionFailureReason.UnsupportedArgs }; } newArgs = [...newArgs]; // Shallow clone the array to avoid setting the default array newArgs[newArgs.length - 1] = format(newArgs[newArgs.length - 1], appRoot); envMixin['VSCODE_STABLE'] = productService.quality === 'stable' ? '1' : '0'; - return { newArgs, envMixin }; + return { type, newArgs, envMixin }; } case 'fish': { if (!originalArgs || originalArgs.length === 0) { @@ -162,7 +170,7 @@ export function getShellIntegrationInjection( newArgs = originalArgs; } if (!newArgs) { - return undefined; + return { type: 'failure', reason: ShellIntegrationInjectionFailureReason.UnsupportedArgs }; } // On fish, '$fish_user_paths' is always prepended to the PATH, for both login and non-login shells, so we need @@ -171,7 +179,7 @@ export function getShellIntegrationInjection( newArgs = [...newArgs]; // Shallow clone the array to avoid setting the default array newArgs[newArgs.length - 1] = format(newArgs[newArgs.length - 1], appRoot); - return { newArgs, envMixin }; + return { type, newArgs, envMixin }; } case 'pwsh': { if (!originalArgs || arePwshImpliedArgs(originalArgs)) { @@ -180,7 +188,7 @@ export function getShellIntegrationInjection( newArgs = shellIntegrationArgs.get(ShellIntegrationExecutable.PwshLogin); } if (!newArgs) { - return undefined; + return { type: 'failure', reason: ShellIntegrationInjectionFailureReason.UnsupportedArgs }; } if (options.shellIntegration.suggestEnabled) { envMixin['VSCODE_SUGGEST'] = '1'; @@ -188,7 +196,7 @@ export function getShellIntegrationInjection( newArgs = [...newArgs]; // Shallow clone the array to avoid setting the default array newArgs[newArgs.length - 1] = format(newArgs[newArgs.length - 1], appRoot, ''); envMixin['VSCODE_STABLE'] = productService.quality === 'stable' ? '1' : '0'; - return { newArgs, envMixin }; + return { type, newArgs, envMixin }; } case 'zsh': { if (!originalArgs || originalArgs.length === 0) { @@ -200,7 +208,7 @@ export function getShellIntegrationInjection( newArgs = originalArgs; } if (!newArgs) { - return undefined; + return { type: 'failure', reason: ShellIntegrationInjectionFailureReason.UnsupportedArgs }; } newArgs = [...newArgs]; // Shallow clone the array to avoid setting the default array newArgs[newArgs.length - 1] = format(newArgs[newArgs.length - 1], appRoot); @@ -233,11 +241,11 @@ export function getShellIntegrationInjection( source: path.join(appRoot, 'out/vs/workbench/contrib/terminal/common/scripts/shellIntegration-login.zsh'), dest: path.join(zdotdir, '.zlogin') }); - return { newArgs, envMixin, filesToCopy }; + return { type, newArgs, envMixin, filesToCopy }; } } logService.warn(`Shell integration cannot be enabled for executable "${shellLaunchConfig.executable}" and args`, shellLaunchConfig.args); - return undefined; + return { type: 'failure', reason: ShellIntegrationInjectionFailureReason.UnsupportedShell }; } /** diff --git a/src/vs/platform/terminal/node/terminalProcess.ts b/src/vs/platform/terminal/node/terminalProcess.ts index e33d5beccb36..71eb27b970c0 100644 --- a/src/vs/platform/terminal/node/terminalProcess.ts +++ b/src/vs/platform/terminal/node/terminalProcess.ts @@ -99,7 +99,8 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess resolvedShellLaunchConfig: {}, overrideDimensions: undefined, failedShellIntegrationActivation: false, - usedShellIntegrationInjection: undefined + usedShellIntegrationInjection: undefined, + shellIntegrationInjectionFailureReason: undefined, }; private static _lastKillOrStart = 0; private _exitCode: number | undefined; @@ -208,39 +209,38 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess return firstError; } - let injection: IShellIntegrationConfigInjection | undefined; - if (this._options.shellIntegration.enabled) { - injection = getShellIntegrationInjection(this.shellLaunchConfig, this._options, this._ptyOptions.env, this._logService, this._productService); - if (injection) { - this._onDidChangeProperty.fire({ type: ProcessPropertyType.UsedShellIntegrationInjection, value: true }); - if (injection.envMixin) { - for (const [key, value] of Object.entries(injection.envMixin)) { - this._ptyOptions.env ||= {}; - this._ptyOptions.env[key] = value; - } + const injection = getShellIntegrationInjection(this.shellLaunchConfig, this._options, this._ptyOptions.env, this._logService, this._productService); + if (injection.type === 'injection') { + this._onDidChangeProperty.fire({ type: ProcessPropertyType.UsedShellIntegrationInjection, value: true }); + if (injection.envMixin) { + for (const [key, value] of Object.entries(injection.envMixin)) { + this._ptyOptions.env ||= {}; + this._ptyOptions.env[key] = value; } - if (injection.filesToCopy) { - for (const f of injection.filesToCopy) { - try { - await fs.promises.mkdir(path.dirname(f.dest), { recursive: true }); - await fs.promises.copyFile(f.source, f.dest); - } catch { - // Swallow error, this should only happen when multiple users are on the same - // machine. Since the shell integration scripts rarely change, plus the other user - // should be using the same version of the server in this case, assume the script is - // fine if copy fails and swallow the error. - } - } - } - } else { - this._onDidChangeProperty.fire({ type: ProcessPropertyType.FailedShellIntegrationActivation, value: true }); } + if (injection.filesToCopy) { + for (const f of injection.filesToCopy) { + try { + await fs.promises.mkdir(path.dirname(f.dest), { recursive: true }); + await fs.promises.copyFile(f.source, f.dest); + } catch { + // Swallow error, this should only happen when multiple users are on the same + // machine. Since the shell integration scripts rarely change, plus the other user + // should be using the same version of the server in this case, assume the script is + // fine if copy fails and swallow the error. + } + } + } + } else { + this._onDidChangeProperty.fire({ type: ProcessPropertyType.FailedShellIntegrationActivation, value: true }); + this._onDidChangeProperty.fire({ type: ProcessPropertyType.ShellIntegrationInjectionFailureReason, value: injection.reason }); } try { - await this.setupPtyProcess(this.shellLaunchConfig, this._ptyOptions, injection); - if (injection?.newArgs) { - return { injectedArgs: injection.newArgs }; + const injectionConfig: IShellIntegrationConfigInjection | undefined = injection.type === 'injection' ? injection : undefined; + await this.setupPtyProcess(this.shellLaunchConfig, this._ptyOptions, injectionConfig); + if (injectionConfig?.newArgs) { + return { injectedArgs: injectionConfig.newArgs }; } return undefined; } catch (err) { diff --git a/src/vs/platform/terminal/test/node/terminalEnvironment.test.ts b/src/vs/platform/terminal/test/node/terminalEnvironment.test.ts index 6248f83003d4..32cdfc541dcb 100644 --- a/src/vs/platform/terminal/test/node/terminalEnvironment.test.ts +++ b/src/vs/platform/terminal/test/node/terminalEnvironment.test.ts @@ -10,7 +10,7 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/c import { NullLogService } from '../../../log/common/log.js'; import { IProductService } from '../../../product/common/productService.js'; import { ITerminalProcessOptions } from '../../common/terminal.js'; -import { getShellIntegrationInjection, getWindowsBuildNumber, IShellIntegrationConfigInjection } from '../../node/terminalEnvironment.js'; +import { getShellIntegrationInjection, getWindowsBuildNumber, IShellIntegrationConfigInjection, type IShellIntegrationInjectionFailure } from '../../node/terminalEnvironment.js'; const enabledProcessOptions: ITerminalProcessOptions = { shellIntegration: { enabled: true, suggestEnabled: false, nonce: '' }, windowsEnableConpty: true, windowsUseConptyDll: false, environmentVariableCollections: undefined, workspaceFolder: undefined }; const disabledProcessOptions: ITerminalProcessOptions = { shellIntegration: { enabled: false, suggestEnabled: false, nonce: '' }, windowsEnableConpty: true, windowsUseConptyDll: false, environmentVariableCollections: undefined, workspaceFolder: undefined }; @@ -21,8 +21,8 @@ const logService = new NullLogService(); const productService = { applicationName: 'vscode' } as IProductService; const defaultEnvironment = {}; -function deepStrictEqualIgnoreStableVar(actual: IShellIntegrationConfigInjection | undefined, expected: IShellIntegrationConfigInjection) { - if (actual?.envMixin) { +function deepStrictEqualIgnoreStableVar(actual: IShellIntegrationConfigInjection | IShellIntegrationInjectionFailure | undefined, expected: IShellIntegrationConfigInjection) { + if (actual && 'envMixin' in actual && actual.envMixin) { delete actual.envMixin['VSCODE_STABLE']; } deepStrictEqual(actual, expected); @@ -34,12 +34,12 @@ suite('platform - terminalEnvironment', () => { suite('should not enable', () => { // This test is only expected to work on Windows 10 build 18309 and above (getWindowsBuildNumber() < 18309 ? test.skip : test)('when isFeatureTerminal or when no executable is provided', () => { - ok(!getShellIntegrationInjection({ executable: pwshExe, args: ['-l', '-NoLogo'], isFeatureTerminal: true }, enabledProcessOptions, defaultEnvironment, logService, productService)); - ok(getShellIntegrationInjection({ executable: pwshExe, args: ['-l', '-NoLogo'], isFeatureTerminal: false }, enabledProcessOptions, defaultEnvironment, logService, productService)); + strictEqual(getShellIntegrationInjection({ executable: pwshExe, args: ['-l', '-NoLogo'], isFeatureTerminal: true }, enabledProcessOptions, defaultEnvironment, logService, productService).type, 'failure'); + strictEqual(getShellIntegrationInjection({ executable: pwshExe, args: ['-l', '-NoLogo'], isFeatureTerminal: false }, enabledProcessOptions, defaultEnvironment, logService, productService).type, 'injection'); }); if (isWindows) { test('when on windows with conpty false', () => { - ok(!getShellIntegrationInjection({ executable: pwshExe, args: ['-l'], isFeatureTerminal: false }, winptyProcessOptions, defaultEnvironment, logService, productService)); + strictEqual(getShellIntegrationInjection({ executable: pwshExe, args: ['-l'], isFeatureTerminal: false }, winptyProcessOptions, defaultEnvironment, logService, productService).type, 'failure'); }); } }); @@ -51,6 +51,7 @@ suite('platform - terminalEnvironment', () => { : `. "${repoRoot}/out/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1"`; suite('should override args', () => { const enabledExpectedResult = Object.freeze({ + type: 'injection', newArgs: [ '-noexit', '-command', @@ -81,6 +82,7 @@ suite('platform - terminalEnvironment', () => { }); suite('should incorporate login arg', () => { const enabledExpectedResult = Object.freeze({ + type: 'injection', newArgs: [ '-l', '-noexit', @@ -100,15 +102,15 @@ suite('platform - terminalEnvironment', () => { }); suite('should not modify args', () => { test('when shell integration is disabled', () => { - strictEqual(getShellIntegrationInjection({ executable: pwshExe, args: ['-l'] }, disabledProcessOptions, defaultEnvironment, logService, productService), undefined); - strictEqual(getShellIntegrationInjection({ executable: pwshExe, args: '-l' }, disabledProcessOptions, defaultEnvironment, logService, productService), undefined); - strictEqual(getShellIntegrationInjection({ executable: pwshExe, args: undefined }, disabledProcessOptions, defaultEnvironment, logService, productService), undefined); + strictEqual(getShellIntegrationInjection({ executable: pwshExe, args: ['-l'] }, disabledProcessOptions, defaultEnvironment, logService, productService).type, 'failure'); + strictEqual(getShellIntegrationInjection({ executable: pwshExe, args: '-l' }, disabledProcessOptions, defaultEnvironment, logService, productService).type, 'failure'); + strictEqual(getShellIntegrationInjection({ executable: pwshExe, args: undefined }, disabledProcessOptions, defaultEnvironment, logService, productService).type, 'failure'); }); test('when using unrecognized arg', () => { - strictEqual(getShellIntegrationInjection({ executable: pwshExe, args: ['-l', '-NoLogo', '-i'] }, disabledProcessOptions, defaultEnvironment, logService, productService), undefined); + strictEqual(getShellIntegrationInjection({ executable: pwshExe, args: ['-l', '-NoLogo', '-i'] }, disabledProcessOptions, defaultEnvironment, logService, productService).type, 'failure'); }); test('when using unrecognized arg (string)', () => { - strictEqual(getShellIntegrationInjection({ executable: pwshExe, args: '-i' }, disabledProcessOptions, defaultEnvironment, logService, productService), undefined); + strictEqual(getShellIntegrationInjection({ executable: pwshExe, args: '-i' }, disabledProcessOptions, defaultEnvironment, logService, productService).type, 'failure'); }); }); }); @@ -147,37 +149,37 @@ suite('platform - terminalEnvironment', () => { ok(result.filesToCopy[3].source.match(expectedSources[3])); } test('when undefined, []', () => { - const result1 = getShellIntegrationInjection({ executable: 'zsh', args: [] }, enabledProcessOptions, defaultEnvironment, logService, productService); + const result1 = getShellIntegrationInjection({ executable: 'zsh', args: [] }, enabledProcessOptions, defaultEnvironment, logService, productService) as IShellIntegrationConfigInjection; deepStrictEqual(result1?.newArgs, ['-i']); assertIsEnabled(result1); - const result2 = getShellIntegrationInjection({ executable: 'zsh', args: undefined }, enabledProcessOptions, defaultEnvironment, logService, productService); + const result2 = getShellIntegrationInjection({ executable: 'zsh', args: undefined }, enabledProcessOptions, defaultEnvironment, logService, productService) as IShellIntegrationConfigInjection; deepStrictEqual(result2?.newArgs, ['-i']); assertIsEnabled(result2); }); suite('should incorporate login arg', () => { test('when array', () => { - const result = getShellIntegrationInjection({ executable: 'zsh', args: ['-l'] }, enabledProcessOptions, defaultEnvironment, logService, productService); + const result = getShellIntegrationInjection({ executable: 'zsh', args: ['-l'] }, enabledProcessOptions, defaultEnvironment, logService, productService) as IShellIntegrationConfigInjection; deepStrictEqual(result?.newArgs, ['-il']); assertIsEnabled(result); }); }); suite('should not modify args', () => { test('when shell integration is disabled', () => { - strictEqual(getShellIntegrationInjection({ executable: 'zsh', args: ['-l'] }, disabledProcessOptions, defaultEnvironment, logService, productService), undefined); - strictEqual(getShellIntegrationInjection({ executable: 'zsh', args: undefined }, disabledProcessOptions, defaultEnvironment, logService, productService), undefined); + strictEqual(getShellIntegrationInjection({ executable: 'zsh', args: ['-l'] }, disabledProcessOptions, defaultEnvironment, logService, productService).type, 'failure'); + strictEqual(getShellIntegrationInjection({ executable: 'zsh', args: undefined }, disabledProcessOptions, defaultEnvironment, logService, productService).type, 'failure'); }); test('when using unrecognized arg', () => { - strictEqual(getShellIntegrationInjection({ executable: 'zsh', args: ['-l', '-fake'] }, disabledProcessOptions, defaultEnvironment, logService, productService), undefined); + strictEqual(getShellIntegrationInjection({ executable: 'zsh', args: ['-l', '-fake'] }, disabledProcessOptions, defaultEnvironment, logService, productService).type, 'failure'); }); }); suite('should incorporate global ZDOTDIR env variable', () => { test('when custom ZDOTDIR', () => { - const result1 = getShellIntegrationInjection({ executable: 'zsh', args: [] }, enabledProcessOptions, { ...defaultEnvironment, ZDOTDIR: customZdotdir }, logService, productService); + const result1 = getShellIntegrationInjection({ executable: 'zsh', args: [] }, enabledProcessOptions, { ...defaultEnvironment, ZDOTDIR: customZdotdir }, logService, productService) as IShellIntegrationConfigInjection; deepStrictEqual(result1?.newArgs, ['-i']); assertIsEnabled(result1, customZdotdir); }); test('when undefined', () => { - const result1 = getShellIntegrationInjection({ executable: 'zsh', args: [] }, enabledProcessOptions, undefined, logService, productService); + const result1 = getShellIntegrationInjection({ executable: 'zsh', args: [] }, enabledProcessOptions, undefined, logService, productService) as IShellIntegrationConfigInjection; deepStrictEqual(result1?.newArgs, ['-i']); assertIsEnabled(result1); }); @@ -188,6 +190,7 @@ suite('platform - terminalEnvironment', () => { suite('should override args', () => { test('when undefined, [], empty string', () => { const enabledExpectedResult = Object.freeze({ + type: 'injection', newArgs: [ '--init-file', `${repoRoot}/out/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh` @@ -202,6 +205,7 @@ suite('platform - terminalEnvironment', () => { }); suite('should set login env variable and not modify args', () => { const enabledExpectedResult = Object.freeze({ + type: 'injection', newArgs: [ '--init-file', `${repoRoot}/out/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh` @@ -217,11 +221,11 @@ suite('platform - terminalEnvironment', () => { }); suite('should not modify args', () => { test('when shell integration is disabled', () => { - strictEqual(getShellIntegrationInjection({ executable: 'bash', args: ['-l'] }, disabledProcessOptions, defaultEnvironment, logService, productService), undefined); - strictEqual(getShellIntegrationInjection({ executable: 'bash', args: undefined }, disabledProcessOptions, defaultEnvironment, logService, productService), undefined); + strictEqual(getShellIntegrationInjection({ executable: 'bash', args: ['-l'] }, disabledProcessOptions, defaultEnvironment, logService, productService).type, 'failure'); + strictEqual(getShellIntegrationInjection({ executable: 'bash', args: undefined }, disabledProcessOptions, defaultEnvironment, logService, productService).type, 'failure'); }); test('when custom array entry', () => { - strictEqual(getShellIntegrationInjection({ executable: 'bash', args: ['-l', '-i'] }, disabledProcessOptions, defaultEnvironment, logService, productService), undefined); + strictEqual(getShellIntegrationInjection({ executable: 'bash', args: ['-l', '-i'] }, disabledProcessOptions, defaultEnvironment, logService, productService).type, 'failure'); }); }); }); diff --git a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts index 54408a57abae..fcd19dbf3ebd 100644 --- a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts +++ b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts @@ -145,13 +145,13 @@ export abstract class AbstractSynchroniser extends Disposable implements IUserDa readonly onDidChangeLocal: Event = this._onDidChangeLocal.event; protected readonly lastSyncResource: URI; - private readonly lastSyncUserDataStateKey = `${this.collection ? `${this.collection}.` : ''}${this.syncResource.syncResource}.lastSyncUserData`; + private readonly lastSyncUserDataStateKey: string; private hasSyncResourceStateVersionChanged: boolean = false; protected readonly syncResourceLogLabel: string; protected syncHeaders: IHeaders = {}; - readonly resource = this.syncResource.syncResource; + readonly resource: SyncResource; constructor( readonly syncResource: IUserDataSyncResource, @@ -168,6 +168,8 @@ export abstract class AbstractSynchroniser extends Disposable implements IUserDa @IUriIdentityService uriIdentityService: IUriIdentityService, ) { super(); + this.lastSyncUserDataStateKey = `${collection ? `${collection}.` : ''}${syncResource.syncResource}.lastSyncUserData`; + this.resource = syncResource.syncResource; this.syncResourceLogLabel = getSyncResourceLogLabel(syncResource.syncResource, syncResource.profile); this.extUri = uriIdentityService.extUri; this.syncFolder = this.extUri.joinPath(environmentService.userDataSyncHome, ...getPathSegments(syncResource.profile.isDefault ? undefined : syncResource.profile.id, syncResource.syncResource)); diff --git a/src/vs/platform/windows/electron-main/windowImpl.ts b/src/vs/platform/windows/electron-main/windowImpl.ts index bcfce8495e95..b3406e345ed4 100644 --- a/src/vs/platform/windows/electron-main/windowImpl.ts +++ b/src/vs/platform/windows/electron-main/windowImpl.ts @@ -147,23 +147,13 @@ export abstract class BaseWindow extends Disposable implements IBaseWindow { } } - // Windows Custom System Context Menu - // See https://github.com/electron/electron/issues/24893 - // - // The purpose of this is to allow for the context menu in the Windows Title Bar - // - // Currently, all mouse events in the title bar are captured by the OS - // thus we need to capture them here with a window hook specific to Windows - // and then forward them to the correct window. + // Setup windows system context menu so it only is allowed in certain cases if (isWindows && useCustomTitleStyle) { - const WM_INITMENU = 0x0116; // https://docs.microsoft.com/en-us/windows/win32/menurc/wm-initmenu - - // This sets up a listener for the window hook. This is a Windows-only API provided by electron. - win.hookWindowMessage(WM_INITMENU, () => { + this._register(Event.fromNodeEventEmitter(win, 'system-context-menu', (event: Electron.Event, point: Electron.Point) => ({ event, point }))((e) => { const [x, y] = win.getPosition(); - const cursorPos = electron.screen.getCursorScreenPoint(); - const cx = cursorPos.x - x; - const cy = cursorPos.y - y; + const cursorPos = electron.screen.screenToDipPoint(e.point); + const cx = Math.floor(cursorPos.x) - x; + const cy = Math.floor(cursorPos.y) - y; // In some cases, show the default system context menu // 1) The mouse position is not within the title bar @@ -181,16 +171,11 @@ export abstract class BaseWindow extends Disposable implements IBaseWindow { }; if (!shouldTriggerDefaultSystemContextMenu()) { - - // This is necessary to make sure the native system context menu does not show up. - win.setEnabled(false); - win.setEnabled(true); + e.event.preventDefault(); this._onDidTriggerSystemContextMenu.fire({ x: cx, y: cy }); } - - return 0; - }); + })); } // Open devtools if instructed from command line args diff --git a/src/vs/workbench/api/browser/mainThreadMcp.ts b/src/vs/workbench/api/browser/mainThreadMcp.ts index 8e87a91b1e0d..097a4a4d16d4 100644 --- a/src/vs/workbench/api/browser/mainThreadMcp.ts +++ b/src/vs/workbench/api/browser/mainThreadMcp.ts @@ -5,17 +5,14 @@ import { disposableTimeout } from '../../../base/common/async.js'; import { Emitter } from '../../../base/common/event.js'; -import { Disposable, DisposableMap, DisposableStore, MutableDisposable } from '../../../base/common/lifecycle.js'; -import { autorun, IObservable, ISettableObservable, observableValue } from '../../../base/common/observable.js'; -import { IConfigurationService } from '../../../platform/configuration/common/configuration.js'; +import { Disposable, DisposableMap } from '../../../base/common/lifecycle.js'; +import { ISettableObservable, observableValue } from '../../../base/common/observable.js'; import { LogLevel } from '../../../platform/log/common/log.js'; -import { observableConfigValue } from '../../../platform/observable/common/platformObservableUtils.js'; -import { mcpEnabledSection } from '../../contrib/mcp/common/mcpConfiguration.js'; import { IMcpMessageTransport, IMcpRegistry } from '../../contrib/mcp/common/mcpRegistryTypes.js'; import { McpCollectionDefinition, McpConnectionState, McpServerDefinition, McpServerTransportType } from '../../contrib/mcp/common/mcpTypes.js'; import { MCP } from '../../contrib/mcp/common/modelContextProtocol.js'; import { ExtensionHostKind, extensionHostKindToString } from '../../services/extensions/common/extensionHostKind.js'; -import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js'; +import { IExtHostContext, extHostNamedCustomer } from '../../services/extensions/common/extHostCustomers.js'; import { ExtHostContext, MainContext, MainThreadMcpShape } from '../common/extHost.protocol.js'; @extHostNamedCustomer(MainContext.MainThreadMcp) @@ -29,17 +26,16 @@ export class MainThreadMcp extends Disposable implements MainThreadMcpShape { servers: ISettableObservable; dispose(): void; }>()); - private readonly _mcpEnabled: IObservable; constructor( private readonly _extHostContext: IExtHostContext, @IMcpRegistry private readonly _mcpRegistry: IMcpRegistry, - @IConfigurationService configurationService: IConfigurationService, ) { super(); const proxy = _extHostContext.getProxy(ExtHostContext.ExtHostMcp); - this._mcpEnabled = observableConfigValue(mcpEnabledSection, true, configurationService); this._register(this._mcpRegistry.registerDelegate({ + // Prefer Node.js extension hosts when they're available. No CORS issues etc. + priority: _extHostContext.extensionHostKind === ExtensionHostKind.LocalWebWorker ? 0 : 1, waitForInitialProviderPromises() { return proxy.$waitForInitialCollectionProviders(); }, @@ -76,25 +72,16 @@ export class MainThreadMcp extends Disposable implements MainThreadMcpShape { existing.servers.set(servers, undefined); } else { const serverDefinitions = observableValue('mcpServers', servers); - - const store = new DisposableStore(); - const handle = store.add(new MutableDisposable()); - store.add(autorun(reader => { - if (this._mcpEnabled.read(reader)) { - handle.value = this._mcpRegistry.registerCollection({ - ...collection, - remoteAuthority: this._extHostContext.remoteAuthority, - serverDefinitions, - }); - } else { - handle.clear(); - } - })); + const handle = this._mcpRegistry.registerCollection({ + ...collection, + remoteAuthority: this._extHostContext.remoteAuthority, + serverDefinitions, + }); this._collectionDefinitions.set(collection.id, { fromExtHost: collection, servers: serverDefinitions, - dispose: () => store.dispose(), + dispose: () => handle.dispose(), }); } } diff --git a/src/vs/workbench/api/browser/mainThreadWebviewViews.ts b/src/vs/workbench/api/browser/mainThreadWebviewViews.ts index 2ce367773d76..b21653ed13ce 100644 --- a/src/vs/workbench/api/browser/mainThreadWebviewViews.ts +++ b/src/vs/workbench/api/browser/mainThreadWebviewViews.ts @@ -5,7 +5,7 @@ import { CancellationToken } from '../../../base/common/cancellation.js'; import { onUnexpectedError } from '../../../base/common/errors.js'; -import { Disposable, DisposableMap } from '../../../base/common/lifecycle.js'; +import { Disposable, DisposableMap, DisposableStore } from '../../../base/common/lifecycle.js'; import { generateUuid } from '../../../base/common/uuid.js'; import { MainThreadWebviews, reviveWebviewExtension } from './mainThreadWebviews.js'; import * as extHostProtocol from '../common/extHost.protocol.js'; @@ -86,14 +86,16 @@ export class MainThreadWebviewsViews extends Disposable implements extHostProtoc webviewView.webview.options = options; } - webviewView.onDidChangeVisibility(visible => { + const subscriptions = new DisposableStore(); + subscriptions.add(webviewView.onDidChangeVisibility(visible => { this._proxy.$onDidChangeWebviewViewVisibility(handle, visible); - }); + })); - webviewView.onDispose(() => { + subscriptions.add(webviewView.onDispose(() => { this._proxy.$disposeWebviewView(handle); this._webviewViews.deleteAndDispose(handle); - }); + subscriptions.dispose(); + })); type CreateWebviewViewTelemetry = { extensionId: string; diff --git a/src/vs/workbench/api/common/extHostExtensionService.ts b/src/vs/workbench/api/common/extHostExtensionService.ts index e6af1f781bed..03aabb460450 100644 --- a/src/vs/workbench/api/common/extHostExtensionService.ts +++ b/src/vs/workbench/api/common/extHostExtensionService.ts @@ -482,10 +482,14 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme this._logService.info(`ExtensionService#_doActivateExtension ${extensionDescription.identifier.value}, startup: ${reason.startup}, activationEvent: '${reason.activationEvent}'${extensionDescription.identifier.value !== reason.extensionId.value ? `, root cause: ${reason.extensionId.value}` : ``}`); this._logService.flush(); + const isESM = this._isESM(extensionDescription); + const extensionInternalStore = new DisposableStore(); // disposables that follow the extension lifecycle const activationTimesBuilder = new ExtensionActivationTimesBuilder(reason.startup); return Promise.all([ - this._loadCommonJSModule(extensionDescription, joinPath(extensionDescription.extensionLocation, entryPoint), activationTimesBuilder), + isESM + ? this._loadESMModule(extensionDescription, joinPath(extensionDescription.extensionLocation, entryPoint), activationTimesBuilder) + : this._loadCommonJSModule(extensionDescription, joinPath(extensionDescription.extensionLocation, entryPoint), activationTimesBuilder), this._loadExtensionContext(extensionDescription, extensionInternalStore) ]).then(values => { performance.mark(`code/extHost/willActivateExtension/${extensionDescription.identifier.value}`); @@ -743,8 +747,13 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme throw new Error(nls.localize('extensionTestError1', "Cannot load test runner.")); } + const extensionDescription = (await this.getExtensionPathIndex()).findSubstr(extensionTestsLocationURI); + const isESM = this._isESM(extensionDescription, extensionTestsLocationURI.path); + // Require the test runner via node require from the provided path - const testRunner = await this._loadCommonJSModule(null, extensionTestsLocationURI, new ExtensionActivationTimesBuilder(false)); + const testRunner = await (isESM + ? this._loadESMModule(null, extensionTestsLocationURI, new ExtensionActivationTimesBuilder(false)) + : this._loadCommonJSModule(null, extensionTestsLocationURI, new ExtensionActivationTimesBuilder(false))); if (!testRunner || typeof testRunner.run !== 'function') { throw new Error(nls.localize('extensionTestError', "Path {0} does not point to a valid extension test runner.", extensionTestsLocationURI.toString())); @@ -1077,9 +1086,15 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme this._onDidChangeRemoteConnectionData.fire(); } + protected _isESM(extensionDescription: IExtensionDescription | undefined, modulePath?: string): boolean { + modulePath ??= extensionDescription?.main; + return modulePath?.endsWith('.mjs') || extensionDescription?.type === 'module'; + } + protected abstract _beforeAlmostReadyToRunExtensions(): Promise; protected abstract _getEntryPoint(extensionDescription: IExtensionDescription): string | undefined; protected abstract _loadCommonJSModule(extensionId: IExtensionDescription | null, module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise; + protected abstract _loadESMModule(extension: IExtensionDescription | null, module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise; public abstract $setRemoteEnvironment(env: { [key: string]: string | null }): Promise; } diff --git a/src/vs/workbench/api/common/extHostRequireInterceptor.ts b/src/vs/workbench/api/common/extHostRequireInterceptor.ts index f01e80dd53ec..e106a29e5759 100644 --- a/src/vs/workbench/api/common/extHostRequireInterceptor.ts +++ b/src/vs/workbench/api/common/extHostRequireInterceptor.ts @@ -27,7 +27,7 @@ interface IAlternativeModuleProvider { alternativeModuleName(name: string): string | undefined; } -interface INodeModuleFactory extends Partial { +export interface INodeModuleFactory extends Partial { readonly nodeModuleName: string | string[]; load(request: string, parent: URI, original: LoadFunction): any; } diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 7b1b68b2e504..75a62af117c8 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -2951,10 +2951,11 @@ export namespace ChatPromptReference { } else if (value && typeof value === 'object' && 'uri' in value && 'range' in value && isUriComponents(value.uri)) { value = Location.to(revive(value)); } else if (variable.isImage) { + const ref = variable.references?.[0]?.reference; value = new types.ChatReferenceBinaryData( variable.mimeType ?? 'image/png', () => Promise.resolve(new Uint8Array(Object.values(variable.value as number[]))), - variable.references && URI.isUri(variable.references[0].reference) ? variable.references[0].reference : undefined + ref && URI.isUri(ref) ? ref : undefined ); } else if (variable.kind === 'diagnostic') { const filterSeverity = variable.filterSeverity && DiagnosticSeverity.to(variable.filterSeverity); diff --git a/src/vs/workbench/api/common/extHostWebviewMessaging.ts b/src/vs/workbench/api/common/extHostWebviewMessaging.ts index faf3d228656f..49f0fa06c990 100644 --- a/src/vs/workbench/api/common/extHostWebviewMessaging.ts +++ b/src/vs/workbench/api/common/extHostWebviewMessaging.ts @@ -7,9 +7,9 @@ import { VSBuffer } from '../../../base/common/buffer.js'; import * as extHostProtocol from './extHost.protocol.js'; class ArrayBufferSet { - public readonly buffers: ArrayBuffer[] = []; + public readonly buffers: ArrayBufferLike[] = []; - public add(buffer: ArrayBuffer): number { + public add(buffer: ArrayBufferLike): number { let index = this.buffers.indexOf(buffer); if (index < 0) { index = this.buffers.length; diff --git a/src/vs/workbench/api/node/extHostExtensionService.ts b/src/vs/workbench/api/node/extHostExtensionService.ts index 7fae836bf435..bb381ed9504a 100644 --- a/src/vs/workbench/api/node/extHostExtensionService.ts +++ b/src/vs/workbench/api/node/extHostExtensionService.ts @@ -4,8 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import * as performance from '../../../base/common/performance.js'; +import type * as vscode from 'vscode'; import { createApiFactoryAndRegisterActors } from '../common/extHost.api.impl.js'; -import { RequireInterceptor } from '../common/extHostRequireInterceptor.js'; +import { INodeModuleFactory, RequireInterceptor } from '../common/extHostRequireInterceptor.js'; import { ExtensionActivationTimesBuilder } from '../common/extHostExtensionActivator.js'; import { connectProxyResolver } from './proxyResolver.js'; import { AbstractExtHostExtensionService } from '../common/extHostExtensionService.js'; @@ -18,8 +19,13 @@ import { CLIServer } from './extHostCLIServer.js'; import { realpathSync } from '../../../base/node/extpath.js'; import { ExtHostConsoleForwarder } from './extHostConsoleForwarder.js'; import { ExtHostDiskFileSystemProvider } from './extHostDiskFileSystemProvider.js'; -import { createRequire } from 'node:module'; -const require = createRequire(import.meta.url); +import nodeModule from 'node:module'; +import { assertType } from '../../../base/common/types.js'; +import { generateUuid } from '../../../base/common/uuid.js'; +import { BidirectionalMap } from '../../../base/common/map.js'; +import { DisposableStore, toDisposable } from '../../../base/common/lifecycle.js'; + +const require = nodeModule.createRequire(import.meta.url); class NodeModuleRequireInterceptor extends RequireInterceptor { @@ -69,6 +75,124 @@ class NodeModuleRequireInterceptor extends RequireInterceptor { } } +class NodeModuleESMInterceptor extends RequireInterceptor { + + private static _createDataUri(scriptContent: string): string { + return `data:text/javascript;base64,${Buffer.from(scriptContent).toString('base64')}`; + } + + // This string is a script that runs in the loader thread of NodeJS. + private static _loaderScript = ` + let lookup; + export const initialize = async (context) => { + let requestIds = 0; + const { port } = context; + const pendingRequests = new Map(); + port.onmessage = (event) => { + const { id, url } = event.data; + pendingRequests.get(id)?.(url); + }; + lookup = url => { + // debugger; + const myId = requestIds++; + return new Promise((resolve) => { + pendingRequests.set(myId, resolve); + port.postMessage({ id: myId, url, }); + }); + }; + }; + export const resolve = async (specifier, context, nextResolve) => { + if (specifier !== 'vscode' || !context.parentURL) { + return nextResolve(specifier, context); + } + const otherUrl = await lookup(context.parentURL); + return { + url: otherUrl, + shortCircuit: true, + }; + };`; + + private static _vscodeImportFnName = `_VSCODE_IMPORT_VSCODE_API`; + + private readonly _store = new DisposableStore(); + + dispose(): void { + this._store.dispose(); + } + + protected override _installInterceptor(): void { + + type Message = { id: string; url: string }; + + const apiInstances = new BidirectionalMap(); + const apiImportDataUrl = new Map(); + + // define a global function that can be used to get API instances given a random key + Object.defineProperty(globalThis, NodeModuleESMInterceptor._vscodeImportFnName, { + enumerable: false, + configurable: false, + writable: false, + value: (key: string) => { + return apiInstances.getKey(key); + } + }); + + const { port1, port2 } = new MessageChannel(); + + let apiModuleFactory: INodeModuleFactory | undefined; + + // this is a workaround for the fact that the layer checker does not understand + // that onmessage is NodeJS API here + const port1LayerCheckerWorkaround: any = port1; + + port1LayerCheckerWorkaround.onmessage = (e: { data: Message }) => { + + // Get the vscode-module factory - which is the same logic that's also used by + // the CommonJS require interceptor + if (!apiModuleFactory) { + apiModuleFactory = this._factories.get('vscode'); + assertType(apiModuleFactory); + } + + const { id, url } = e.data; + const uri = URI.parse(url); + + // Get or create the API instance. The interface is per extension and extensions are + // looked up by the uri (e.data.url) and path containment. + const apiInstance = apiModuleFactory.load('_not_used', uri, () => { throw new Error('CANNOT LOAD MODULE from here.'); }); + let key = apiInstances.get(apiInstance); + if (!key) { + key = generateUuid(); + apiInstances.set(apiInstance, key); + } + + // Create and cache a data-url which is the import script for the API instance + let scriptDataUrlSrc = apiImportDataUrl.get(key); + if (!scriptDataUrlSrc) { + const jsCode = `const _vscodeInstance = globalThis.${NodeModuleESMInterceptor._vscodeImportFnName}('${key}');\n\n${Object.keys(apiInstance).map((name => `export const ${name} = _vscodeInstance['${name}'];`)).join('\n')}`; + scriptDataUrlSrc = NodeModuleESMInterceptor._createDataUri(jsCode); + apiImportDataUrl.set(key, scriptDataUrlSrc); + } + + port1.postMessage({ + id, + url: scriptDataUrlSrc + }); + }; + + nodeModule.register(NodeModuleESMInterceptor._createDataUri(NodeModuleESMInterceptor._loaderScript), { + parentURL: import.meta.url, + data: { port: port2 }, + transferList: [port2], + }); + + this._store.add(toDisposable(() => { + port1.close(); + port2.close(); + })); + } +} + export class ExtHostExtensionService extends AbstractExtHostExtensionService { readonly extensionRuntime = ExtensionRuntime.Node; @@ -93,8 +217,13 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { this._instaService.createInstance(ExtHostDiskFileSystemProvider); // Module loading tricks - const interceptor = this._instaService.createInstance(NodeModuleRequireInterceptor, extensionApiFactory, { mine: this._myRegistry, all: this._globalRegistry }); - await interceptor.install(); + await this._instaService.createInstance(NodeModuleRequireInterceptor, extensionApiFactory, { mine: this._myRegistry, all: this._globalRegistry }) + .install(); + + // ESM loading tricks + await this._store.add(this._instaService.createInstance(NodeModuleESMInterceptor, extensionApiFactory, { mine: this._myRegistry, all: this._globalRegistry })) + .install(); + performance.mark('code/extHost/didInitAPI'); // Do this when extension service exists, but extensions are not being activated yet. @@ -107,13 +236,13 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { return extensionDescription.main; } - protected async _loadCommonJSModule(extension: IExtensionDescription | null, module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise { + private async _doLoadModule(extension: IExtensionDescription | null, module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder, mode: 'esm' | 'cjs'): Promise { if (module.scheme !== Schemas.file) { throw new Error(`Cannot load URI: '${module}', must be of file-scheme`); } let r: T | null = null; activationTimesBuilder.codeLoadingStart(); - this._logService.trace(`ExtensionService#loadCommonJSModule ${module.toString(true)}`); + this._logService.trace(`ExtensionService#loadModule [${mode}] -> ${module.toString(true)}`); this._logService.flush(); const extensionId = extension?.identifier.value; if (extension) { @@ -123,7 +252,11 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { if (extensionId) { performance.mark(`code/extHost/willLoadExtensionCode/${extensionId}`); } - r = (require)(module.fsPath); + if (mode === 'esm') { + r = await import(module.fsPath); + } else { + r = require(module.fsPath); + } } finally { if (extensionId) { performance.mark(`code/extHost/didLoadExtensionCode/${extensionId}`); @@ -133,6 +266,14 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { return r; } + protected async _loadCommonJSModule(extension: IExtensionDescription | null, module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise { + return this._doLoadModule(extension, module, activationTimesBuilder, 'cjs'); + } + + protected async _loadESMModule(extension: IExtensionDescription | null, module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise { + return this._doLoadModule(extension, module, activationTimesBuilder, 'esm'); + } + public async $setRemoteEnvironment(env: { [key: string]: string | null }): Promise { if (!this._initData.remote.isRemote) { return; diff --git a/src/vs/workbench/api/worker/extHostExtensionService.ts b/src/vs/workbench/api/worker/extHostExtensionService.ts index 6f3384ab89c2..46bb2a0970f0 100644 --- a/src/vs/workbench/api/worker/extHostExtensionService.ts +++ b/src/vs/workbench/api/worker/extHostExtensionService.ts @@ -125,6 +125,10 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { } } + protected override _loadESMModule(extension: IExtensionDescription | null, module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise { + throw new Error('ESM modules are not supported in the web worker extension host'); + } + async $setRemoteEnvironment(_env: { [key: string]: string | null }): Promise { return; } diff --git a/src/vs/workbench/browser/dnd.ts b/src/vs/workbench/browser/dnd.ts index 72def8c4b99c..16bddbd0ea17 100644 --- a/src/vs/workbench/browser/dnd.ts +++ b/src/vs/workbench/browser/dnd.ts @@ -216,7 +216,11 @@ export function fillEditorsDragData(accessor: ServicesAccessor, resourcesOrEdito return undefined; // editor without resource } - return { ...resourceOrEditor, resource: resourceOrEditor.selection ? withSelection(resourceOrEditor.resource, resourceOrEditor.selection) : resourceOrEditor.resource }; + return { + resource: resourceOrEditor.selection ? withSelection(resourceOrEditor.resource, resourceOrEditor.selection) : resourceOrEditor.resource, + isDirectory: resourceOrEditor.isDirectory, + selection: resourceOrEditor.selection, + }; })); const fileSystemResources = resources.filter(({ resource }) => fileService.hasProvider(resource)); @@ -334,9 +338,12 @@ export function fillEditorsDragData(accessor: ServicesAccessor, resourcesOrEdito if (draggedEditors.length) { event.dataTransfer.setData(CodeDataTransfers.EDITORS, stringify(draggedEditors)); + } - // Add a URI list entry - const uriListEntries: URI[] = []; + // Add a URI list entry + const draggedDirectories: URI[] = fileSystemResources.filter(({ isDirectory }) => isDirectory).map(({ resource }) => resource); + if (draggedEditors.length || draggedDirectories.length) { + const uriListEntries: URI[] = [...draggedDirectories]; for (const editor of draggedEditors) { if (editor.resource) { uriListEntries.push(editor.options?.selection ? withSelection(editor.resource, editor.options.selection) : editor.resource); diff --git a/src/vs/workbench/browser/parts/banner/bannerPart.ts b/src/vs/workbench/browser/parts/banner/bannerPart.ts index 8495bd425bc6..aa0ed529debe 100644 --- a/src/vs/workbench/browser/parts/banner/bannerPart.ts +++ b/src/vs/workbench/browser/parts/banner/bannerPart.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import './media/bannerpart.css'; -import { localize2 } from '../../../../nls.js'; +import { localize, localize2 } from '../../../../nls.js'; import { $, addDisposableListener, append, clearNode, EventType, isHTMLElement } from '../../../../base/browser/dom.js'; import { asCSSUrl } from '../../../../base/browser/cssValue.js'; import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js'; @@ -225,7 +225,7 @@ export class BannerPart extends Part implements IBannerService { // Action const actionBarContainer = append(this.element, $('div.action-container')); this.actionBar = this._register(new ActionBar(actionBarContainer)); - const label = item.closeLabel ?? 'Close Banner'; + const label = item.closeLabel ?? localize('closeBanner', "Close Banner"); const closeAction = this._register(new Action('banner.close', label, ThemeIcon.asClassName(widgetClose), true, () => this.close(item))); this.actionBar.push(closeAction, { icon: true, label: false }); this.actionBar.setFocusable(false); diff --git a/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts b/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts index ac461ef94384..d4db3411faf3 100644 --- a/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts +++ b/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts @@ -114,12 +114,9 @@ export class BrowserDialogHandler extends AbstractDialogHandler { customOptions.markdownDetails?.forEach(markdownDetail => { const result = this.markdownRenderer.render(markdownDetail.markdown, { actionHandler: { - callback: link => { - if (markdownDetail.dismissOnLinkClick) { - dialog.dispose(); - } + callback: markdownDetail.actionHandler || (link => { return openLinkFromMarkdown(this.openerService, link, markdownDetail.markdown.isTrusted, true /* skip URL validation to prevent another dialog from showing which is unsupported */); - }, + }), disposables: dialogDisposables } }); diff --git a/src/vs/workbench/browser/parts/editor/editorCommands.ts b/src/vs/workbench/browser/parts/editor/editorCommands.ts index 83a724a79b1f..9463dfc1de3e 100644 --- a/src/vs/workbench/browser/parts/editor/editorCommands.ts +++ b/src/vs/workbench/browser/parts/editor/editorCommands.ts @@ -61,6 +61,7 @@ export const LOCK_GROUP_COMMAND_ID = 'workbench.action.lockEditorGroup'; export const UNLOCK_GROUP_COMMAND_ID = 'workbench.action.unlockEditorGroup'; export const SHOW_EDITORS_IN_GROUP = 'workbench.action.showEditorsInGroup'; export const REOPEN_WITH_COMMAND_ID = 'workbench.action.reopenWithEditor'; +export const REOPEN_ACTIVE_EDITOR_WITH_COMMAND_ID = 'reopenActiveEditorWith'; export const PIN_EDITOR_COMMAND_ID = 'workbench.action.pinEditor'; export const UNPIN_EDITOR_COMMAND_ID = 'workbench.action.unpinEditor'; @@ -867,74 +868,88 @@ function registerCloseEditorCommands() { weight: KeybindingWeight.WorkbenchContrib, when: undefined, primary: undefined, - handler: async (accessor, ...args: unknown[]) => { - const editorService = accessor.get(IEditorService); - const editorResolverService = accessor.get(IEditorResolverService); - const telemetryService = accessor.get(ITelemetryService); - - const resolvedContext = resolveCommandsContext(args, editorService, accessor.get(IEditorGroupsService), accessor.get(IListService)); - const editorReplacements = new Map(); - - for (const { group, editors } of resolvedContext.groupedEditors) { - for (const editor of editors) { - const untypedEditor = editor.toUntyped(); - if (!untypedEditor) { - return; // Resolver can only resolve untyped editors - } - - untypedEditor.options = { ...editorService.activeEditorPane?.options, override: EditorResolution.PICK }; - const resolvedEditor = await editorResolverService.resolveEditor(untypedEditor, group); - if (!isEditorInputWithOptionsAndGroup(resolvedEditor)) { - return; - } - - let editorReplacementsInGroup = editorReplacements.get(group); - if (!editorReplacementsInGroup) { - editorReplacementsInGroup = []; - editorReplacements.set(group, editorReplacementsInGroup); - } - - editorReplacementsInGroup.push({ - editor: editor, - replacement: resolvedEditor.editor, - forceReplaceDirty: editor.resource?.scheme === Schemas.untitled, - options: resolvedEditor.options - }); - - // Telemetry - type WorkbenchEditorReopenClassification = { - owner: 'rebornix'; - comment: 'Identify how a document is reopened'; - scheme: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'File system provider scheme for the resource' }; - ext: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'File extension for the resource' }; - from: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The editor view type the resource is switched from' }; - to: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The editor view type the resource is switched to' }; - }; - - type WorkbenchEditorReopenEvent = { - scheme: string; - ext: string; - from: string; - to: string; - }; - - telemetryService.publicLog2('workbenchEditorReopen', { - scheme: editor.resource?.scheme ?? '', - ext: editor.resource ? extname(editor.resource) : '', - from: editor.editorId ?? '', - to: resolvedEditor.editor.editorId ?? '' - }); - } - } - - // Replace editor with resolved one and make active - for (const [group, replacements] of editorReplacements) { - await group.replaceEditors(replacements); - await group.openEditor(replacements[0].replacement); - } + handler: (accessor, ...args: unknown[]) => { + return reopenEditorWith(accessor, EditorResolution.PICK, ...args); } }); + KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: REOPEN_ACTIVE_EDITOR_WITH_COMMAND_ID, + weight: KeybindingWeight.WorkbenchContrib, + when: undefined, + primary: undefined, + handler: (accessor, override?: string, ...args: unknown[]) => { + return reopenEditorWith(accessor, override ?? EditorResolution.PICK, ...args); + } + }); + + async function reopenEditorWith(accessor: ServicesAccessor, editorOverride: string | EditorResolution, ...args: unknown[]) { + const editorService = accessor.get(IEditorService); + const editorResolverService = accessor.get(IEditorResolverService); + const telemetryService = accessor.get(ITelemetryService); + + const resolvedContext = resolveCommandsContext(args, editorService, accessor.get(IEditorGroupsService), accessor.get(IListService)); + const editorReplacements = new Map(); + + for (const { group, editors } of resolvedContext.groupedEditors) { + for (const editor of editors) { + const untypedEditor = editor.toUntyped(); + if (!untypedEditor) { + return; // Resolver can only resolve untyped editors + } + + untypedEditor.options = { ...editorService.activeEditorPane?.options, override: editorOverride }; + const resolvedEditor = await editorResolverService.resolveEditor(untypedEditor, group); + if (!isEditorInputWithOptionsAndGroup(resolvedEditor)) { + return; + } + + let editorReplacementsInGroup = editorReplacements.get(group); + if (!editorReplacementsInGroup) { + editorReplacementsInGroup = []; + editorReplacements.set(group, editorReplacementsInGroup); + } + + editorReplacementsInGroup.push({ + editor: editor, + replacement: resolvedEditor.editor, + forceReplaceDirty: editor.resource?.scheme === Schemas.untitled, + options: resolvedEditor.options + }); + + // Telemetry + type WorkbenchEditorReopenClassification = { + owner: 'rebornix'; + comment: 'Identify how a document is reopened'; + scheme: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'File system provider scheme for the resource' }; + ext: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'File extension for the resource' }; + from: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The editor view type the resource is switched from' }; + to: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The editor view type the resource is switched to' }; + }; + + type WorkbenchEditorReopenEvent = { + scheme: string; + ext: string; + from: string; + to: string; + }; + + telemetryService.publicLog2('workbenchEditorReopen', { + scheme: editor.resource?.scheme ?? '', + ext: editor.resource ? extname(editor.resource) : '', + from: editor.editorId ?? '', + to: resolvedEditor.editor.editorId ?? '' + }); + } + } + + // Replace editor with resolved one and make active + for (const [group, replacements] of editorReplacements) { + await group.replaceEditors(replacements); + await group.openEditor(replacements[0].replacement); + } + } + CommandsRegistry.registerCommand(CLOSE_EDITORS_AND_GROUP_COMMAND_ID, async (accessor: ServicesAccessor, ...args: unknown[]) => { const editorGroupsService = accessor.get(IEditorGroupsService); diff --git a/src/vs/workbench/browser/parts/notifications/media/notificationsCenter.css b/src/vs/workbench/browser/parts/notifications/media/notificationsCenter.css index 2025ec3ea7da..79a7fd74d335 100644 --- a/src/vs/workbench/browser/parts/notifications/media/notificationsCenter.css +++ b/src/vs/workbench/browser/parts/notifications/media/notificationsCenter.css @@ -6,8 +6,8 @@ .monaco-workbench > .notifications-center { position: absolute; z-index: 1000; - right: 11px; /* attempt to position at same location as a toast */ - bottom: 33px; /* 22px status bar height + 11px (attempt to position at same location as a toast) */ + right: 7px; /* attempt to position at same location as a toast */ + bottom: 29px; /* 22px status bar height + 7px (attempt to position at same location as a toast) */ display: none; overflow: hidden; border-radius: 4px; diff --git a/src/vs/workbench/browser/parts/notifications/media/notificationsToasts.css b/src/vs/workbench/browser/parts/notifications/media/notificationsToasts.css index f12d6b925dfa..033a0026ebd6 100644 --- a/src/vs/workbench/browser/parts/notifications/media/notificationsToasts.css +++ b/src/vs/workbench/browser/parts/notifications/media/notificationsToasts.css @@ -26,7 +26,7 @@ } .monaco-workbench > .notifications-toasts .notification-toast-container > .notification-toast { - margin: 8px; /* enables separation and drop shadows around toasts */ + margin: 4px; /* enables separation and drop shadows around toasts */ transform: translate3d(0px, 100%, 0px); /* move the notification 50px to the bottom (to prevent bleed through) */ opacity: 0; /* fade the toast in */ transition: transform 300ms ease-out, opacity 300ms ease-out; diff --git a/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css b/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css index 9e1e2041ab8d..80de2a657b93 100644 --- a/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css +++ b/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css @@ -73,32 +73,12 @@ border-right: 5px solid transparent; } -.monaco-workbench .part.statusbar > .items-container > .statusbar-item > .statusbar-item-label { - margin-right: 3px; - margin-left: 3px; -} - -.monaco-workbench .part.statusbar > .items-container > .statusbar-item.compact-left > .statusbar-item-label { - margin-right: 3px; - margin-left: 0; -} - -.monaco-workbench .part.statusbar > .items-container > .statusbar-item.compact-right > .statusbar-item-label { - margin-right: 0; - margin-left: 3px; -} - -.monaco-workbench .part.statusbar > .items-container > .statusbar-item.compact-left.compact-right > .statusbar-item-label { - margin-right:0; - margin-left: 0; -} - .monaco-workbench .part.statusbar > .items-container > .statusbar-item.left.first-visible-item { padding-left: 7px; /* Add padding to the most left status bar item */ } .monaco-workbench .part.statusbar > .items-container > .statusbar-item.right.last-visible-item { - padding-right: 7px; /* Add padding to the most right status bar item */ + margin-right: 7px; /* Add margin to the most right status bar item. Margin is used to position beak properly. */ } /* Tweak appearance for items with background to improve hover feedback */ @@ -108,9 +88,40 @@ padding-left: 0; } -.monaco-workbench .part.statusbar > .items-container > .statusbar-item.has-background-color > .statusbar-item-label { - margin-right: 0; +.monaco-workbench .part.statusbar > .items-container > .statusbar-item > .statusbar-item-label { + cursor: pointer; + display: flex; + height: 100%; + margin-right: 3px; + margin-left: 3px; + padding: 0 5px 0 5px; + white-space: pre; /* gives some degree of styling */ + align-items: center; + text-overflow: ellipsis; + overflow: hidden; + outline-width: 0px; /* do not render focus outline, we already have background */ +} + +.monaco-workbench .part.statusbar > .items-container > .statusbar-item.compact-left > .statusbar-item-label { margin-left: 0; + margin-right: 5px; /* +2px because padding is smaller and we want to preserve spacing between items */ + padding: 0 3px; +} + +.monaco-workbench .part.statusbar > .items-container > .statusbar-item.compact-right > .statusbar-item-label { + margin-left: 5px; /* +2px because padding is smaller and we want to preserve spacing between items */ + margin-right: 0; + padding: 0 3px; +} + +.monaco-workbench .part.statusbar > .items-container > .statusbar-item.compact-left.compact-right > .statusbar-item-label { + margin-left: 0; + margin-right:0; +} + +.monaco-workbench .part.statusbar > .items-container > .statusbar-item.has-background-color > .statusbar-item-label { + margin-left: 0; + margin-right: 0; padding-left: 10px; padding-right: 10px; } @@ -125,26 +136,6 @@ padding-right: 3px; } -.monaco-workbench .part.statusbar > .items-container > .statusbar-item > .statusbar-item-label { - cursor: pointer; - display: flex; - height: 100%; - padding: 0 5px 0 5px; - white-space: pre; /* gives some degree of styling */ - align-items: center; - text-overflow: ellipsis; - overflow: hidden; - outline-width: 0px; /* do not render focus outline, we already have background */ -} - -.monaco-workbench .part.statusbar > .items-container > .statusbar-item.compact-left > .statusbar-item-label { - padding: 0 3px; -} - -.monaco-workbench .part.statusbar > .items-container > .statusbar-item.compact-right > .statusbar-item-label { - padding: 0 3px; -} - .monaco-workbench .part.statusbar > .items-container > .statusbar-item > a:hover:not(.disabled) { text-decoration: none; color: var(--vscode-statusBarItem-hoverForeground); diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts index 2e6ce3bc4a2c..4cb6b8f40003 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts @@ -675,7 +675,7 @@ class StatusbarPart extends Part implements IStatusbarEntryContainer { /* Notification Beak */ .monaco-workbench .part.statusbar > .items-container > .statusbar-item.has-beak > .status-bar-item-beak-container:before { - border-bottom-color: ${backgroundColor}; + border-bottom-color: ${borderColor ?? backgroundColor}; } `; } diff --git a/src/vs/workbench/browser/parts/titlebar/titlebarActions.ts b/src/vs/workbench/browser/parts/titlebar/titlebarActions.ts index e09172e4a7f2..15f57f6ad52e 100644 --- a/src/vs/workbench/browser/parts/titlebar/titlebarActions.ts +++ b/src/vs/workbench/browser/parts/titlebar/titlebarActions.ts @@ -14,7 +14,6 @@ import { ACCOUNTS_ACTIVITY_ID, GLOBAL_ACTIVITY_ID } from '../../../common/activi import { IAction } from '../../../../base/common/actions.js'; import { IsAuxiliaryWindowFocusedContext, IsMainWindowFullscreenContext, TitleBarStyleContext, TitleBarVisibleContext } from '../../../common/contextkeys.js'; import { CustomTitleBarVisibility, TitleBarSetting, TitlebarStyle } from '../../../../platform/window/common/window.js'; -import { isLinux, isNative } from '../../../../base/common/platform.js'; // --- Context Menu Actions --- // @@ -259,26 +258,6 @@ registerAction2(class ToggleEditorActions extends Action2 { } }); -if (isLinux && isNative) { - registerAction2(class ToggleCustomTitleBar extends Action2 { - constructor() { - super({ - id: `toggle.${TitleBarSetting.TITLE_BAR_STYLE}`, - title: localize('toggle.titleBarStyle', 'Restore Native Title Bar'), - menu: [ - { id: MenuId.TitleBarContext, order: 0, when: ContextKeyExpr.equals(TitleBarStyleContext.key, TitlebarStyle.CUSTOM), group: '4_restore_native_title' }, - { id: MenuId.TitleBarTitleContext, order: 0, when: ContextKeyExpr.equals(TitleBarStyleContext.key, TitlebarStyle.CUSTOM), group: '4_restore_native_title' }, - ] - }); - } - - run(accessor: ServicesAccessor): void { - const configService = accessor.get(IConfigurationService); - configService.updateValue(TitleBarSetting.TITLE_BAR_STYLE, TitlebarStyle.NATIVE); - } - }); -} - // --- Toolbar actions --- // export const ACCOUNTS_ACTIVITY_TILE_ACTION: IAction = { diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts index 6e000778220c..409a37c512c0 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts @@ -12,6 +12,7 @@ import { Codicon } from '../../../../base/common/codicons.js'; import { KeyCode } from '../../../../base/common/keyCodes.js'; import { Disposable, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js'; import * as marked from '../../../../base/common/marked/marked.js'; +import { Schemas } from '../../../../base/common/network.js'; import { isMacintosh, isWindows } from '../../../../base/common/platform.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { URI } from '../../../../base/common/uri.js'; @@ -21,10 +22,11 @@ import { CodeEditorWidget, ICodeEditorWidgetOptions } from '../../../../editor/b import { IPosition, Position } from '../../../../editor/common/core/position.js'; import { ITextModel } from '../../../../editor/common/model.js'; import { IModelService } from '../../../../editor/common/services/model.js'; +import { ITextModelContentProvider, ITextModelService } from '../../../../editor/common/services/resolverService.js'; import { AccessibilityHelpNLS } from '../../../../editor/common/standaloneStrings.js'; import { CodeActionController } from '../../../../editor/contrib/codeAction/browser/codeActionController.js'; import { localize } from '../../../../nls.js'; -import { AccessibleViewProviderId, AccessibleViewType, AccessibleContentProvider, ExtensionContentProvider, IAccessibleViewService, IAccessibleViewSymbol, isIAccessibleViewContentProvider } from '../../../../platform/accessibility/browser/accessibleView.js'; +import { AccessibleContentProvider, AccessibleViewProviderId, AccessibleViewType, ExtensionContentProvider, IAccessibleViewService, IAccessibleViewSymbol, isIAccessibleViewContentProvider } from '../../../../platform/accessibility/browser/accessibleView.js'; import { ACCESSIBLE_VIEW_SHOWN_STORAGE_PREFIX, IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js'; import { getFlatActionBarActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; import { WorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; @@ -40,14 +42,13 @@ import { ILayoutService } from '../../../../platform/layout/browser/layoutServic import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import { IQuickInputService, IQuickPick, IQuickPickItem } from '../../../../platform/quickinput/common/quickInput.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; -import { AccessibilityVerbositySettingId, AccessibilityWorkbenchSettingId, accessibilityHelpIsShown, accessibleViewContainsCodeBlocks, accessibleViewCurrentProviderId, accessibleViewGoToSymbolSupported, accessibleViewHasAssignedKeybindings, accessibleViewHasUnassignedKeybindings, accessibleViewInCodeBlock, accessibleViewIsShown, accessibleViewOnLastLine, accessibleViewSupportsNavigation, accessibleViewVerbosityEnabled } from './accessibilityConfiguration.js'; -import { resolveContentAndKeybindingItems } from './accessibleViewKeybindingResolver.js'; -import { AccessibilityCommandId } from '../common/accessibilityCommands.js'; +import { FloatingEditorClickMenu } from '../../../browser/codeeditor.js'; import { IChatCodeBlockContextProviderService } from '../../chat/browser/chat.js'; import { ICodeBlockActionContext } from '../../chat/browser/codeBlockPart.js'; import { getSimpleEditorOptions } from '../../codeEditor/browser/simpleEditorOptions.js'; -import { Schemas } from '../../../../base/common/network.js'; -import { ITextModelContentProvider, ITextModelService } from '../../../../editor/common/services/resolverService.js'; +import { AccessibilityCommandId } from '../common/accessibilityCommands.js'; +import { AccessibilityVerbositySettingId, AccessibilityWorkbenchSettingId, accessibilityHelpIsShown, accessibleViewContainsCodeBlocks, accessibleViewCurrentProviderId, accessibleViewGoToSymbolSupported, accessibleViewHasAssignedKeybindings, accessibleViewHasUnassignedKeybindings, accessibleViewInCodeBlock, accessibleViewIsShown, accessibleViewOnLastLine, accessibleViewSupportsNavigation, accessibleViewVerbosityEnabled } from './accessibilityConfiguration.js'; +import { resolveContentAndKeybindingItems } from './accessibleViewKeybindingResolver.js'; const enum DIMENSIONS { MAX_WIDTH = 600 @@ -130,7 +131,7 @@ export class AccessibleView extends Disposable implements ITextModelContentProvi this._container.classList.add('hide'); } const codeEditorWidgetOptions: ICodeEditorWidgetOptions = { - contributions: EditorExtensionsRegistry.getEditorContributions().filter(c => c.id !== CodeActionController.ID) + contributions: EditorExtensionsRegistry.getEditorContributions().filter(c => c.id !== CodeActionController.ID && c.id !== FloatingEditorClickMenu.ID) }; const titleBar = document.createElement('div'); titleBar.classList.add('accessible-view-title-bar'); diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleViewActions.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleViewActions.ts index f587bd7cb1ee..6e9adf5de616 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibleViewActions.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleViewActions.ts @@ -330,7 +330,7 @@ class AccessibleViewAcceptInlineCompletionAction extends Action2 { return; } const model = InlineCompletionsController.get(editor)?.model.get(); - const state = model?.inlineCompletionState.get(); + const state = model?.state.get(); if (!model || !state) { return; } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index a58125f0bd1e..33199529b5e3 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -44,7 +44,7 @@ import { IWorkbenchLayoutService, Parts } from '../../../../services/layout/brow import { IViewsService } from '../../../../services/views/common/viewsService.js'; import { EXTENSIONS_CATEGORY, IExtensionsWorkbenchService } from '../../../extensions/common/extensions.js'; import { ChatContextKeys } from '../../common/chatContextKeys.js'; -import { IChatEditingSession, WorkingSetEntryState } from '../../common/chatEditingService.js'; +import { IChatEditingSession, ModifiedFileEntryState } from '../../common/chatEditingService.js'; import { ChatEntitlement, ChatSentiment, IChatEntitlementService } from '../../common/chatEntitlementService.js'; import { extractAgentAndCommand } from '../../common/chatParserTypes.js'; import { IChatDetail, IChatService } from '../../common/chatService.js'; @@ -824,7 +824,7 @@ export async function showClearEditingSessionConfirmation(editingSession: IChatE const title = options?.titleOverride ?? defaultTitle; const currentEdits = editingSession.entries.get(); - const undecidedEdits = currentEdits.filter((edit) => edit.state.get() === WorkingSetEntryState.Modified); + const undecidedEdits = currentEdits.filter((edit) => edit.state.get() === ModifiedFileEntryState.Modified); const { result } = await dialogService.prompt({ title, @@ -857,7 +857,7 @@ export function shouldShowClearEditingSessionConfirmation(editingSession: IChatE const currentEditCount = currentEdits.length; if (currentEditCount) { - const undecidedEdits = currentEdits.filter((edit) => edit.state.get() === WorkingSetEntryState.Modified); + const undecidedEdits = currentEdits.filter((edit) => edit.state.get() === ModifiedFileEntryState.Modified); return !!undecidedEdits.length; } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index 0260214b072e..531f419ffe41 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -17,7 +17,7 @@ import { KeybindingWeight } from '../../../../../platform/keybinding/common/keyb import { IViewsService } from '../../../../services/views/common/viewsService.js'; import { IChatAgentService } from '../../common/chatAgents.js'; import { ChatContextKeyExprs, ChatContextKeys } from '../../common/chatContextKeys.js'; -import { WorkingSetEntryState } from '../../common/chatEditingService.js'; +import { ModifiedFileEntryState } from '../../common/chatEditingService.js'; import { chatVariableLeader } from '../../common/chatParserTypes.js'; import { IChatService } from '../../common/chatService.js'; import { ChatAgentLocation, ChatConfiguration, ChatMode, validateChatMode } from '../../common/constants.js'; @@ -173,7 +173,7 @@ class ToggleChatModeAction extends Action2 { } const currentEdits = context.editingSession.entries.get(); - const undecidedEdits = currentEdits.filter((edit) => edit.state.get() === WorkingSetEntryState.Modified); + const undecidedEdits = currentEdits.filter((edit) => edit.state.get() === ModifiedFileEntryState.Modified); if (undecidedEdits.length > 0) { if (!await handleCurrentEditingSession(context.editingSession, phrase, dialogService)) { return; diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts index 71bfc203f7de..9715d8e85ab6 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts @@ -5,39 +5,28 @@ import { Codicon } from '../../../../../base/common/codicons.js'; import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; -import { DisposableStore } from '../../../../../base/common/lifecycle.js'; -import { ResourceSet } from '../../../../../base/common/map.js'; import { marked } from '../../../../../base/common/marked/marked.js'; -import { observableFromEvent, waitForState } from '../../../../../base/common/observable.js'; import { basename } from '../../../../../base/common/resources.js'; -import { URI } from '../../../../../base/common/uri.js'; import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; import { IBulkEditService } from '../../../../../editor/browser/services/bulkEditService.js'; -import { isLocation } from '../../../../../editor/common/languages.js'; import { localize, localize2 } from '../../../../../nls.js'; import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; 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 { ILogService } from '../../../../../platform/log/common/log.js'; -import { IQuickInputService, IQuickPickItem } from '../../../../../platform/quickinput/common/quickInput.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; -import { IViewsService } from '../../../../services/views/common/viewsService.js'; import { ResourceNotebookCellEdit } from '../../../bulkEdit/browser/bulkCellEdits.js'; import { MENU_INLINE_CHAT_WIDGET_SECONDARY } from '../../../inlineChat/common/inlineChat.js'; import { INotebookEditor } from '../../../notebook/browser/notebookBrowser.js'; import { CellEditType, CellKind, NOTEBOOK_EDITOR_ID } from '../../../notebook/common/notebookCommon.js'; import { NOTEBOOK_IS_ACTIVE_EDITOR } from '../../../notebook/common/notebookContextKeys.js'; -import { IChatAgentService } from '../../common/chatAgents.js'; import { ChatContextKeyExprs, ChatContextKeys } from '../../common/chatContextKeys.js'; -import { applyingChatEditsFailedContextKey, ChatEditingSessionState, IChatEditingService, isChatEditingActionContext } from '../../common/chatEditingService.js'; -import { IChatRequestModel } from '../../common/chatModel.js'; +import { applyingChatEditsFailedContextKey, isChatEditingActionContext } from '../../common/chatEditingService.js'; import { ChatAgentVoteDirection, ChatAgentVoteDownReason, IChatService } from '../../common/chatService.js'; import { isRequestVM, isResponseVM } from '../../common/chatViewModel.js'; import { ChatAgentLocation, ChatMode } from '../../common/constants.js'; -import { ChatTreeItem, EditsViewId, IChatWidgetService } from '../chat.js'; -import { ChatViewPane } from '../chatViewPane.js'; +import { ChatTreeItem, IChatWidgetService } from '../chat.js'; import { CHAT_CATEGORY } from './chatActions.js'; export const MarkUnhelpfulActionId = 'workbench.action.chat.markUnhelpful'; @@ -408,224 +397,6 @@ export function registerChatTitleActions() { } } }); - - registerAction2(class ContinueEditingAction extends Action2 { - constructor() { - super({ - id: 'workbench.action.chat.startEditing', - title: localize2('chat.startEditing.label2', "Edit with Copilot"), - f1: false, - category: CHAT_CATEGORY, - icon: Codicon.goToEditingSession, - precondition: ContextKeyExpr.and( - ChatContextKeys.editingParticipantRegistered, - ChatContextKeys.requestInProgress.toNegated(), - ChatContextKeys.location.isEqualTo(ChatAgentLocation.Panel), - ChatContextKeyExprs.unifiedChatEnabled.negate() - ), - menu: { - id: MenuId.ChatMessageFooter, - group: 'navigation', - order: 4, - when: ContextKeyExpr.and(ChatContextKeys.enabled, ChatContextKeys.isResponse, ChatContextKeys.editingParticipantRegistered, ChatContextKeys.location.isEqualTo(ChatAgentLocation.Panel), ChatContextKeyExprs.unifiedChatEnabled.negate()) - } - }); - } - - async run(accessor: ServicesAccessor, ...args: any[]) { - - const logService = accessor.get(ILogService); - const chatWidgetService = accessor.get(IChatWidgetService); - const chatService = accessor.get(IChatService); - const chatAgentService = accessor.get(IChatAgentService); - const viewsService = accessor.get(IViewsService); - const chatEditingService = accessor.get(IChatEditingService); - const quickPickService = accessor.get(IQuickInputService); - - const editAgent = chatAgentService.getDefaultAgent(ChatAgentLocation.EditingSession); - if (!editAgent) { - logService.trace('[CHAT_MOVE] No edit agent found'); - return; - } - - const sourceWidget = chatWidgetService.lastFocusedWidget; - if (!sourceWidget || !sourceWidget.viewModel) { - logService.trace('[CHAT_MOVE] NO source model'); - return; - } - - const sourceModel = sourceWidget.viewModel.model; - let sourceRequests = sourceModel.getRequests().slice(); - - // when a response is passed (clicked on) ignore all item after it - const [first] = args; - if (isResponseVM(first)) { - const idx = sourceRequests.findIndex(candidate => candidate.id === first.requestId); - if (idx >= 0) { - sourceRequests.length = idx + 1; - } - } - - // when having multiple turns, let the user pick - if (sourceRequests.length > 1) { - sourceRequests = await this._pickTurns(quickPickService, sourceRequests); - } - - if (sourceRequests.length === 0) { - logService.trace('[CHAT_MOVE] NO requests to move'); - return; - } - - const editsView = await viewsService.openView(EditsViewId); - - if (!(editsView instanceof ChatViewPane)) { - return; - } - - const viewModelObs = observableFromEvent(this, editsView.widget.onDidChangeViewModel, () => editsView.widget.viewModel); - const chatSessionId = (await waitForState(viewModelObs)).sessionId; - const editingSession = chatEditingService.getEditingSession(chatSessionId); - - if (!editingSession) { - return; - } - - const state = editingSession.state.get(); - if (state === ChatEditingSessionState.Disposed) { - return; - } - - // adopt request items and collect new working set entries - const workingSetAdditions = new ResourceSet(); - for (const request of sourceRequests) { - await chatService.adoptRequest(editingSession.chatSessionId, request); - this._collectWorkingSetAdditions(request, workingSetAdditions); - } - await Promise.all(Array.from(workingSetAdditions, async uri => editsView.widget.attachmentModel.addFile(uri))); - - // make request - await chatService.sendRequest(editingSession.chatSessionId, '', { - agentId: editAgent.id, - acceptedConfirmationData: [{ _type: 'toEditTransfer', transferredTurnResults: sourceRequests.map(v => v.response?.result) }], // TODO@jrieken HACKY - confirmation: typeof this.desc.title === 'string' ? this.desc.title : this.desc.title.value - }); - } - - private _collectWorkingSetAdditions(request: IChatRequestModel, bucket: ResourceSet) { - for (const item of request.response?.response.value ?? []) { - if (item.kind === 'inlineReference') { - bucket.add(isLocation(item.inlineReference) - ? item.inlineReference.uri - : URI.isUri(item.inlineReference) - ? item.inlineReference - : item.inlineReference.location.uri - ); - } - } - } - - private async _pickTurns(quickPickService: IQuickInputService, requests: IChatRequestModel[]): Promise { - - const timeThreshold = 2 * 60000; // 2 minutes - const lastRequestTimestamp = requests[requests.length - 1].timestamp; - const relatedRequests = requests.filter(request => request.timestamp >= 0 && lastRequestTimestamp - request.timestamp <= timeThreshold); - - const lastPick: IQuickPickItem = { - label: localize('chat.startEditing.last', "The last {0} requests", relatedRequests.length), - detail: relatedRequests.map(req => req.message.text).join(', ') - }; - - const allPick: IQuickPickItem = { - label: localize('chat.startEditing.pickAll', "All requests from the conversation") - }; - - const customPick: IQuickPickItem = { - label: localize('chat.startEditing.pickCustom', "Manually select requests...") - }; - - const picks: IQuickPickItem[] = relatedRequests.length !== 0 - ? [lastPick, allPick, customPick] - : [allPick, customPick]; - - const firstPick = await quickPickService.pick(picks, { - placeHolder: localize('chat.startEditing.pickRequest', "Select requests that you want to use for editing") - }); - - if (!firstPick) { - return []; - } else if (firstPick === allPick) { - return requests; - } else if (firstPick === lastPick) { - return relatedRequests; - } - - // custom pick - type PickType = (IQuickPickItem & { request: IChatRequestModel }); - const customPicks: (IQuickPickItem & { request: IChatRequestModel })[] = requests.map(request => ({ - - picked: false, - request: request, - label: request.message.text, - detail: request.response?.response.toString(), - })); - - - return await new Promise(_resolve => { - - const resolve = (value: IChatRequestModel[]) => { - store.dispose(); - _resolve(value); - qp.hide(); - }; - - const store = new DisposableStore(); - - const qp = quickPickService.createQuickPick(); - qp.placeholder = localize('chat.startEditing.pickRequest', "Select requests that you want to use for editing"); - qp.canSelectMany = true; - qp.items = customPicks; - - let ignore = false; - store.add(qp.onDidChangeSelection(e => { - if (ignore) { - return; - } - ignore = true; - try { - const [first] = e; - - const selected: typeof customPicks = []; - let disabled = false; - - for (let i = 0; i < customPicks.length; i++) { - const oldItem = customPicks[i]; - customPicks[i] = { - ...oldItem, - disabled, - }; - - disabled = disabled || oldItem === first; - - if (disabled) { - selected.push(customPicks[i]); - } - } - qp.items = customPicks; - qp.selectedItems = selected; - - } finally { - ignore = false; - } - })); - - store.add(qp.onDidAccept(_e => resolve(qp.selectedItems.map(i => i.request)))); - store.add(qp.onDidHide(_ => resolve([]))); - store.add(qp); - qp.show(); - }); - } - - }); } interface MarkdownContent { diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatTransfer.ts b/src/vs/workbench/contrib/chat/browser/actions/chatTransfer.ts index 52bc36801707..8825330db974 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatTransfer.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatTransfer.ts @@ -14,6 +14,6 @@ export class ChatTransferContribution extends Disposable implements IWorkbenchCo @IChatTransferService chatTransferService: IChatTransferService, ) { super(); - chatTransferService.checkAndSetWorkspaceTrust(); + chatTransferService.checkAndSetTransferredWorkspaceTrust(); } } diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 5c61e6ab2e97..993326833228 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -631,6 +631,7 @@ registerChatToolActions(); registerEditorFeature(ChatPasteProvidersFeature); +registerSingleton(IChatTransferService, ChatTransferService, InstantiationType.Delayed); registerSingleton(IChatService, ChatService, InstantiationType.Delayed); registerSingleton(IChatWidgetService, ChatWidgetService, InstantiationType.Delayed); registerSingleton(IQuickChatService, QuickChatService, InstantiationType.Delayed); @@ -651,6 +652,5 @@ registerSingleton(IChatMarkdownAnchorService, ChatMarkdownAnchorService, Instant registerSingleton(ILanguageModelIgnoredFilesService, LanguageModelIgnoredFilesService, InstantiationType.Delayed); registerSingleton(IChatEntitlementService, ChatEntitlementService, InstantiationType.Delayed); registerSingleton(IPromptsService, PromptsService, InstantiationType.Delayed); -registerSingleton(IChatTransferService, ChatTransferService, InstantiationType.Delayed); registerWorkbenchContribution2(ChatEditingNotebookFileSystemProviderContrib.ID, ChatEditingNotebookFileSystemProviderContrib, WorkbenchPhase.BlockStartup); diff --git a/src/vs/workbench/contrib/chat/browser/chatAttachmentResolve.ts b/src/vs/workbench/contrib/chat/browser/chatAttachmentResolve.ts new file mode 100644 index 000000000000..9bf667fb3c19 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatAttachmentResolve.ts @@ -0,0 +1,195 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { basename } 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 { SymbolKinds } from '../../../../editor/common/languages.js'; +import { ITextModelService } from '../../../../editor/common/services/resolverService.js'; +import { localize } from '../../../../nls.js'; +import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; +import { IDraggedResourceEditorInput, MarkerTransferData, DocumentSymbolTransferData } from '../../../../platform/dnd/browser/dnd.js'; +import { IFileService } from '../../../../platform/files/common/files.js'; +import { MarkerSeverity } from '../../../../platform/markers/common/markers.js'; +import { isUntitledResourceEditorInput } from '../../../common/editor.js'; +import { EditorInput } from '../../../common/editor/editorInput.js'; +import { IEditorService } from '../../../services/editor/common/editorService.js'; +import { IExtensionService, isProposedApiEnabled } from '../../../services/extensions/common/extensions.js'; +import { UntitledTextEditorInput } from '../../../services/untitled/common/untitledTextEditorInput.js'; +import { IChatRequestVariableEntry, IDiagnosticVariableEntry, IDiagnosticVariableEntryFilterData, ISymbolVariableEntry } from '../common/chatModel.js'; +import { imageToHash } from './chatPasteProviders.js'; +import { resizeImage } from './imageUtils.js'; + +// --- EDITORS --- + +export async function resolveEditorAttachContext(editor: EditorInput | IDraggedResourceEditorInput, fileService: IFileService, editorService: IEditorService, textModelService: ITextModelService, extensionService: IExtensionService, dialogService: IDialogService): Promise { + // untitled editor + if (isUntitledResourceEditorInput(editor)) { + return await resolveUntitledEditorAttachContext(editor, editorService, textModelService); + } + + if (!editor.resource) { + return undefined; + } + + let stat; + try { + stat = await fileService.stat(editor.resource); + } catch { + return undefined; + } + + if (!stat.isDirectory && !stat.isFile) { + return undefined; + } + + const imageContext = await resolveImageEditorAttachContext(editor, fileService, dialogService); + if (imageContext) { + return extensionService.extensions.some(ext => isProposedApiEnabled(ext, 'chatReferenceBinaryData')) ? imageContext : undefined; + } + + return await resolveResourceAttachContext(editor.resource, stat.isDirectory, textModelService); +} + +async function resolveUntitledEditorAttachContext(editor: IDraggedResourceEditorInput, editorService: IEditorService, textModelService: ITextModelService): Promise { + // If the resource is known, we can use it directly + if (editor.resource) { + return await resolveResourceAttachContext(editor.resource, false, textModelService); + } + + // Otherwise, we need to check if the contents are already open in another editor + const openUntitledEditors = editorService.editors.filter(editor => editor instanceof UntitledTextEditorInput) as UntitledTextEditorInput[]; + for (const canidate of openUntitledEditors) { + const model = await canidate.resolve(); + const contents = model.textEditorModel?.getValue(); + if (contents === editor.contents) { + return await resolveResourceAttachContext(canidate.resource, false, textModelService); + } + } + + return undefined; +} + +export async function resolveResourceAttachContext(resource: URI, isDirectory: boolean, textModelService: ITextModelService): Promise { + let isOmitted = false; + + if (!isDirectory) { + try { + const createdModel = await textModelService.createModelReference(resource); + createdModel.dispose(); + } catch { + isOmitted = true; + } + + if (/\.(svg)$/i.test(resource.path)) { + isOmitted = true; + } + } + + return { + value: resource, + id: resource.toString(), + name: basename(resource), + isFile: !isDirectory, + isDirectory, + isOmitted + }; +} + +// --- IMAGES --- + +export type ImageTransferData = { + data: Uint8Array; + name: string; + icon?: ThemeIcon; + resource?: URI; + id?: string; +}; +const SUPPORTED_IMAGE_EXTENSIONS_REGEX = /\.(png|jpg|jpeg|gif|webp)$/i; + +export async function resolveImageEditorAttachContext(editor: EditorInput | IDraggedResourceEditorInput, fileService: IFileService, dialogService: IDialogService): Promise { + if (!editor.resource) { + return undefined; + } + + if (!SUPPORTED_IMAGE_EXTENSIONS_REGEX.test(editor.resource.path)) { + return undefined; + } + + const fileName = basename(editor.resource); + const readFile = await fileService.readFile(editor.resource); + if (readFile.size > 30 * 1024 * 1024) { // 30 MB + dialogService.error(localize('imageTooLarge', 'Image is too large'), localize('imageTooLargeMessage', 'The image {0} is too large to be attached.', fileName)); + throw new Error('Image is too large'); + } + + const imageFileContext = await resolveImageAttachContext([{ + id: editor.resource.toString(), + name: fileName, + data: readFile.value.buffer, + icon: Codicon.fileMedia, + resource: editor.resource, + }]); + + return imageFileContext[0]; +} + +export async function resolveImageAttachContext(images: ImageTransferData[]): Promise { + return Promise.all(images.map(async image => ({ + id: image.id || await imageToHash(image.data), + name: image.name, + fullName: image.resource ? image.resource.path : undefined, + value: await resizeImage(image.data), + icon: image.icon, + isImage: true, + isFile: false, + isDirectory: false, + references: image.resource ? [{ reference: image.resource, kind: 'reference' }] : [] + }))); +} + +// --- MARKERS --- + +export function resolveMarkerAttachContext(markers: MarkerTransferData[]): IDiagnosticVariableEntry[] { + return markers.map((marker): IDiagnosticVariableEntry => { + let filter: IDiagnosticVariableEntryFilterData; + if (!('severity' in marker)) { + filter = { filterUri: URI.revive(marker.uri), filterSeverity: MarkerSeverity.Warning }; + } else { + filter = IDiagnosticVariableEntryFilterData.fromMarker(marker); + } + + return IDiagnosticVariableEntryFilterData.toEntry(filter); + }); +} + +// --- SYMBOLS --- + +export function resolveSymbolsAttachContext(symbols: DocumentSymbolTransferData[]): ISymbolVariableEntry[] { + return symbols.map(symbol => { + const resource = URI.file(symbol.fsPath); + return { + kind: 'symbol', + id: symbolId(resource, symbol.range), + value: { uri: resource, range: symbol.range }, + symbolKind: symbol.kind, + fullName: `$(${SymbolKinds.toIcon(symbol.kind).id}) ${symbol.name}`, + name: symbol.name, + }; + }); +} + +function symbolId(resource: URI, range?: IRange): string { + let rangePart = ''; + if (range) { + rangePart = `:${range.startLineNumber}`; + if (range.startLineNumber !== range.endLineNumber) { + rangePart += `-${range.endLineNumber}`; + } + } + return resource.fsPath + rangePart; +} diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts index 3d5115ef8cd1..8544b9b44420 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts @@ -37,7 +37,7 @@ import { ResourceContextKey } from '../../../../common/contextkeys.js'; import { SETTINGS_AUTHORITY } from '../../../../services/preferences/common/preferences.js'; import { createFileIconThemableTreeContainerScope } from '../../../files/browser/views/explorerView.js'; import { ExplorerFolderContext } from '../../../files/common/files.js'; -import { chatEditingWidgetFileStateContextKey, WorkingSetEntryState } from '../../common/chatEditingService.js'; +import { chatEditingWidgetFileStateContextKey, ModifiedFileEntryState } from '../../common/chatEditingService.js'; import { ChatResponseReferencePartStatusKind, IChatContentReference, IChatWarningMessage } from '../../common/chatService.js'; import { IChatVariablesService } from '../../common/chatVariables.js'; import { IChatRendererContent, IChatResponseViewModel } from '../../common/chatViewModel.js'; @@ -51,7 +51,7 @@ export const $ = dom.$; export interface IChatReferenceListItem extends IChatContentReference { title?: string; description?: string; - state?: WorkingSetEntryState; + state?: ModifiedFileEntryState; excluded?: boolean; } @@ -402,10 +402,10 @@ class CollapsibleListRenderer implements IListRenderer = new Map(); @@ -154,7 +150,7 @@ export class ChatDragAndDrop extends Themable { } private async drop(e: DragEvent): Promise { - const contexts = await this.getAttachContext(e); + const contexts = await this.resolveAttachmentsFromDragEvent(e); if (contexts.length === 0) { return; } @@ -173,7 +169,7 @@ export class ChatDragAndDrop extends Themable { private guessDropType(e: DragEvent): ChatDragAndDropType | undefined { // This is an esstimation based on the datatransfer types/items - if (this.isImageDnd(e)) { + if (containsImageDragType(e)) { return this.extensionService.extensions.some(ext => isProposedApiEnabled(ext, 'chatReferenceBinaryData')) ? ChatDragAndDropType.IMAGE : undefined; } else if (containsDragType(e, 'text/html')) { return ChatDragAndDropType.HTML; @@ -210,121 +206,33 @@ export class ChatDragAndDrop extends Themable { } } - private isImageDnd(e: DragEvent): boolean { - // Image detection should not have false positives, only false negatives are allowed - if (containsDragType(e, 'image')) { - return true; - } - - if (containsDragType(e, DataTransfers.FILES)) { - const files = e.dataTransfer?.files; - if (files && files.length > 0) { - const file = files[0]; - return file.type.startsWith('image/'); - } - - const items = e.dataTransfer?.items; - if (items && items.length > 0) { - const item = items[0]; - return item.type.startsWith('image/'); - } - } - - return false; - } - - private async getAttachContext(e: DragEvent): Promise { + private async resolveAttachmentsFromDragEvent(e: DragEvent): Promise { if (!this.isDragEventSupported(e)) { return []; } const markerData = extractMarkerDropData(e); if (markerData) { - return this.resolveMarkerAttachContext(markerData); + return resolveMarkerAttachContext(markerData); } if (containsDragType(e, CodeDataTransfers.SYMBOLS)) { - const data = extractSymbolDropData(e); - return this.resolveSymbolsAttachContext(data); + const symbolsData = extractSymbolDropData(e); + return resolveSymbolsAttachContext(symbolsData); } - if (!containsDragType(e, DataTransfers.INTERNAL_URI_LIST) && containsDragType(e, Mimes.uriList) && ((containsDragType(e, Mimes.html) || containsDragType(e, Mimes.text)))) { + const editorDragData = extractEditorsDropData(e); + if (editorDragData.length > 0) { + return coalesce(await Promise.all(editorDragData.map(editorInput => { + return resolveEditorAttachContext(editorInput, this.fileService, this.editorService, this.textModelService, this.extensionService, this.dialogService); + }))); + } + + if (!containsDragType(e, DataTransfers.INTERNAL_URI_LIST) && containsDragType(e, Mimes.uriList) && ((containsDragType(e, Mimes.html) || containsDragType(e, Mimes.text) /* Text mime needed for safari support */))) { return this.resolveHTMLAttachContext(e); } - const data = extractEditorsDropData(e); - return coalesce(await Promise.all(data.map(editorInput => { - return this.resolveAttachContext(editorInput); - }))); - } - - private async resolveAttachContext(editorInput: IDraggedResourceEditorInput): Promise { - // Image - const imageContext = await getImageAttachContext(editorInput, this.fileService, this.dialogService); - if (imageContext) { - return this.extensionService.extensions.some(ext => isProposedApiEnabled(ext, 'chatReferenceBinaryData')) ? imageContext : undefined; - } - - // File - return await this.getEditorAttachContext(editorInput); - } - - private async getEditorAttachContext(editor: EditorInput | IDraggedResourceEditorInput): Promise { - - // untitled editor - if (isUntitledResourceEditorInput(editor)) { - return await this.resolveUntitledAttachContext(editor); - } - - if (!editor.resource) { - return undefined; - } - - let stat; - try { - stat = await this.fileService.stat(editor.resource); - } catch { - return undefined; - } - - if (!stat.isDirectory && !stat.isFile) { - return undefined; - } - - return await getResourceAttachContext(editor.resource, stat.isDirectory, this.textModelService); - } - - private async resolveUntitledAttachContext(editor: IDraggedResourceEditorInput): Promise { - // If the resource is known, we can use it directly - if (editor.resource) { - return await getResourceAttachContext(editor.resource, false, this.textModelService); - } - - // Otherwise, we need to check if the contents are already open in another editor - const openUntitledEditors = this.editorService.editors.filter(editor => editor instanceof UntitledTextEditorInput) as UntitledTextEditorInput[]; - for (const canidate of openUntitledEditors) { - const model = await canidate.resolve(); - const contents = model.textEditorModel?.getValue(); - if (contents === editor.contents) { - return await getResourceAttachContext(canidate.resource, false, this.textModelService); - } - } - - return undefined; - } - - private resolveSymbolsAttachContext(symbols: DocumentSymbolTransferData[]): ISymbolVariableEntry[] { - return symbols.map(symbol => { - const resource = URI.file(symbol.fsPath); - return { - kind: 'symbol', - id: symbolId(resource, symbol.range), - value: { uri: resource, range: symbol.range }, - symbolKind: symbol.kind, - fullName: `$(${SymbolKinds.toIcon(symbol.kind).id}) ${symbol.name}`, - name: symbol.name, - }; - }); + return []; } private async downloadImageAsUint8Array(url: string): Promise { @@ -348,59 +256,65 @@ export class ChatDragAndDrop extends Themable { } private async resolveHTMLAttachContext(e: DragEvent): Promise { - const displayName = localize('dragAndDroppedImageName', 'Image from URL'); - let finalDisplayName = displayName; + const existingAttachmentNames = new Set(this.attachmentModel.attachments.map(attachment => attachment.name)); + const createDisplayName = (): string => { + const baseName = localize('dragAndDroppedImageName', 'Image from URL'); + let uniqueName = baseName; + let baseNameInstance = 1; - for (let appendValue = 2; this.attachmentModel.attachments.some(attachment => attachment.name === finalDisplayName); appendValue++) { - finalDisplayName = `${displayName} ${appendValue}`; - } + while (existingAttachmentNames.has(uniqueName)) { + uniqueName = `${baseName} ${++baseNameInstance}`; + } - const dataFromFile = await this.extractImageFromFile(e); - if (dataFromFile) { - return [await this.createImageVariable(await resizeImage(dataFromFile), finalDisplayName)]; - } + existingAttachmentNames.add(uniqueName); + return uniqueName; + }; - const dataFromUrl = await this.extractImageFromUrl(e); - const variableEntries: IChatRequestVariableEntry[] = []; - if (dataFromUrl) { - for (const url of dataFromUrl) { - if (/^data:image\/[a-z]+;base64,/.test(url)) { - variableEntries.push(await this.createImageVariable(await resizeImage(url), finalDisplayName, URI.parse(url))); - } else if (/^https?:\/\/.+/.test(url)) { - const imageData = await this.downloadImageAsUint8Array(url); - if (imageData) { - variableEntries.push(await this.createImageVariable(await resizeImage(imageData), finalDisplayName, URI.parse(url), url)); - } + const getImageTransferDataFromUrl = async (url: string): Promise => { + const resource = URI.parse(url); + + if (IMAGE_DATA_REGEX.test(url)) { + return { data: await convertStringToUInt8Array(url), name: createDisplayName(), resource }; + } + + if (URL_REGEX.test(url)) { + const data = await this.downloadImageAsUint8Array(url); + if (data) { + return { data, name: createDisplayName(), resource, id: url }; } } - } - return variableEntries; - } - - private async createImageVariable(data: Uint8Array, name: string, uri?: URI, id?: string,): Promise { - return { - id: id || await imageToHash(data), - name: name, - value: data, - isImage: true, - isFile: false, - isDirectory: false, - references: uri ? [{ reference: uri, kind: 'reference' }] : [] + return undefined; }; - } - private resolveMarkerAttachContext(markers: MarkerTransferData[]): IDiagnosticVariableEntry[] { - return markers.map((marker): IDiagnosticVariableEntry => { - let filter: IDiagnosticVariableEntryFilterData; - if (!('severity' in marker)) { - filter = { filterUri: URI.revive(marker.uri), filterSeverity: MarkerSeverity.Warning }; - } else { - filter = IDiagnosticVariableEntryFilterData.fromMarker(marker); + const getImageTransferDataFromFile = async (file: File): Promise => { + try { + const buffer = await file.arrayBuffer(); + return { data: new Uint8Array(buffer), name: createDisplayName() }; + } catch (error) { + this.logService.error('Error reading file:', error); } - return IDiagnosticVariableEntryFilterData.toEntry(filter); - }); + return undefined; + }; + + const imageTransferData: ImageTransferData[] = []; + + // Image Web File Drag and Drop + const imageFiles = extractImageFilesFromDragEvent(e); + if (imageFiles.length) { + const imageTransferDataFromFiles = await Promise.all(imageFiles.map(file => getImageTransferDataFromFile(file))); + imageTransferData.push(...imageTransferDataFromFiles.filter(data => !!data)); + } + + // Image Web URL Drag and Drop + const imageUrls = extractUrlsFromDragEvent(e); + if (imageUrls.length) { + const imageTransferDataFromUrl = await Promise.all(imageUrls.map(getImageTransferDataFromUrl)); + imageTransferData.push(...imageTransferDataFromUrl.filter(data => !!data)); + } + + return await resolveImageAttachContext(imageTransferData); } private setOverlay(target: HTMLElement, type: ChatDragAndDropType | undefined): void { @@ -442,108 +356,51 @@ export class ChatDragAndDrop extends Themable { this.overlays.forEach(overlay => this.updateOverlayStyles(overlay.overlay)); this.overlayTextBackground = this.getColor(this.styles.listBackground) || ''; } +} +function containsImageDragType(e: DragEvent): boolean { + // Image detection should not have false positives, only false negatives are allowed + if (containsDragType(e, 'image')) { + return true; + } - - private async extractImageFromFile(e: DragEvent): Promise { + if (containsDragType(e, DataTransfers.FILES)) { const files = e.dataTransfer?.files; if (files && files.length > 0) { - const file = files[0]; - if (file.type.startsWith('image/')) { - try { - const buffer = await file.arrayBuffer(); - return new Uint8Array(buffer); - } catch (error) { - this.logService.error('Error reading file:', error); - return undefined; - } - } + return Array.from(files).some(file => file.type.startsWith('image/')); } - return undefined; - } - - private async extractImageFromUrl(e: DragEvent): Promise { - const textUrl = e.dataTransfer?.getData('text/uri-list'); - if (textUrl) { - try { - const uris = UriList.parse(textUrl); - if (uris.length > 0) { - return uris; - } - } catch (error) { - this.logService.error('Error parsing URI list:', error); - return undefined; - } + const items = e.dataTransfer?.items; + if (items && items.length > 0) { + return Array.from(items).some(item => item.type.startsWith('image/')); } - - return undefined; } - + return false; } -async function getResourceAttachContext(resource: URI, isDirectory: boolean, textModelService: ITextModelService): Promise { - let isOmitted = false; - - if (!isDirectory) { +function extractUrlsFromDragEvent(e: DragEvent, logService?: ILogService): string[] { + const textUrl = e.dataTransfer?.getData('text/uri-list'); + if (textUrl) { try { - const createdModel = await textModelService.createModelReference(resource); - createdModel.dispose(); - } catch { - isOmitted = true; - } - - if (/\.(svg)$/i.test(resource.path)) { - isOmitted = true; + const urls = UriList.parse(textUrl); + if (urls.length > 0) { + return urls; + } + } catch (error) { + logService?.error('Error parsing URI list:', error); + return []; } } - return { - value: resource, - id: resource.toString(), - name: basename(resource), - isFile: !isDirectory, - isDirectory, - isOmitted - }; + return []; } -async function getImageAttachContext(editor: EditorInput | IDraggedResourceEditorInput, fileService: IFileService, dialogService: IDialogService): Promise { - if (!editor.resource) { - return undefined; +function extractImageFilesFromDragEvent(e: DragEvent): File[] { + const files = e.dataTransfer?.files; + if (!files) { + return []; } - if (/\.(png|jpg|jpeg|gif|webp)$/i.test(editor.resource.path)) { - const fileName = basename(editor.resource); - const readFile = await fileService.readFile(editor.resource); - if (readFile.size > 30 * 1024 * 1024) { // 30 MB - dialogService.error(localize('imageTooLarge', 'Image is too large'), localize('imageTooLargeMessage', 'The image {0} is too large to be attached.', fileName)); - throw new Error('Image is too large'); - } - const resizedImage = await resizeImage(readFile.value.buffer); - return { - id: editor.resource.toString(), - name: fileName, - fullName: editor.resource.path, - value: resizedImage, - icon: Codicon.fileMedia, - isImage: true, - isFile: false, - references: [{ reference: editor.resource, kind: 'reference' }] - }; - } - - return undefined; -} - -function symbolId(resource: URI, range?: IRange): string { - let rangePart = ''; - if (range) { - rangePart = `:${range.startLineNumber}`; - if (range.startLineNumber !== range.endLineNumber) { - rangePart += `-${range.endLineNumber}`; - } - } - return resource.fsPath + rangePart; + return Array.from(files).filter(file => file.type.startsWith('image/')); } diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts index fa505f7d3cb1..c983381771e1 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts @@ -28,7 +28,7 @@ import { GroupsOrder, IEditorGroupsService } from '../../../../services/editor/c import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { isChatViewTitleActionContext } from '../../common/chatActions.js'; import { ChatContextKeys } from '../../common/chatContextKeys.js'; -import { applyingChatEditsFailedContextKey, CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME, chatEditingResourceContextKey, chatEditingWidgetFileStateContextKey, decidedChatEditingResourceContextKey, hasAppliedChatEditsContextKey, hasUndecidedChatEditingResourceContextKey, IChatEditingService, IChatEditingSession, WorkingSetEntryRemovalReason, WorkingSetEntryState } from '../../common/chatEditingService.js'; +import { applyingChatEditsFailedContextKey, CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME, chatEditingResourceContextKey, chatEditingWidgetFileStateContextKey, decidedChatEditingResourceContextKey, hasAppliedChatEditsContextKey, hasUndecidedChatEditingResourceContextKey, IChatEditingService, IChatEditingSession, ModifiedFileEntryState } from '../../common/chatEditingService.js'; import { IChatService } from '../../common/chatService.js'; import { isRequestVM, isResponseVM } from '../../common/chatViewModel.js'; import { ChatAgentLocation, ChatMode } from '../../common/constants.js'; @@ -127,7 +127,7 @@ registerAction2(class RemoveFileFromWorkingSet extends WorkingSetAction { async runWorkingSetAction(accessor: ServicesAccessor, currentEditingSession: IChatEditingSession, chatWidget: IChatWidget, ...uris: URI[]): Promise { const dialogService = accessor.get(IDialogService); - const pendingEntries = currentEditingSession.entries.get().filter((entry) => uris.includes(entry.modifiedURI) && entry.state.get() === WorkingSetEntryState.Modified); + const pendingEntries = currentEditingSession.entries.get().filter((entry) => uris.includes(entry.modifiedURI) && entry.state.get() === ModifiedFileEntryState.Modified); if (pendingEntries.length > 0) { // Ask for confirmation if there are any pending edits const file = pendingEntries.length > 1 @@ -146,7 +146,7 @@ registerAction2(class RemoveFileFromWorkingSet extends WorkingSetAction { // Remove from working set await currentEditingSession.reject(...uris); - currentEditingSession.remove(WorkingSetEntryRemovalReason.User, ...uris); + currentEditingSession.remove(...uris); // Remove from chat input part for (const uri of uris) { @@ -168,7 +168,7 @@ registerAction2(class OpenFileInDiffAction extends WorkingSetAction { icon: Codicon.diffSingle, menu: [{ id: MenuId.ChatEditingWidgetModifiedFilesToolbar, - when: ContextKeyExpr.equals(chatEditingWidgetFileStateContextKey.key, WorkingSetEntryState.Modified), + when: ContextKeyExpr.equals(chatEditingWidgetFileStateContextKey.key, ModifiedFileEntryState.Modified), order: 2, group: 'navigation' }], @@ -179,7 +179,7 @@ registerAction2(class OpenFileInDiffAction extends WorkingSetAction { const editorService = accessor.get(IEditorService); for (const uri of uris) { const editedFile = currentEditingSession.getEntry(uri); - if (editedFile?.state.get() === WorkingSetEntryState.Modified) { + if (editedFile?.state.get() === ModifiedFileEntryState.Modified) { await editorService.openEditor({ original: { resource: URI.from(editedFile.originalURI, true) }, modified: { resource: URI.from(editedFile.modifiedURI, true) }, @@ -205,7 +205,7 @@ registerAction2(class AcceptAction extends WorkingSetAction { group: 'navigation', }, { id: MenuId.ChatEditingWidgetModifiedFilesToolbar, - when: ContextKeyExpr.equals(chatEditingWidgetFileStateContextKey.key, WorkingSetEntryState.Modified), + when: ContextKeyExpr.equals(chatEditingWidgetFileStateContextKey.key, ModifiedFileEntryState.Modified), order: 0, group: 'navigation' }], @@ -231,7 +231,7 @@ registerAction2(class DiscardAction extends WorkingSetAction { group: 'navigation', }, { id: MenuId.ChatEditingWidgetModifiedFilesToolbar, - when: ContextKeyExpr.equals(chatEditingWidgetFileStateContextKey.key, WorkingSetEntryState.Modified), + when: ContextKeyExpr.equals(chatEditingWidgetFileStateContextKey.key, ModifiedFileEntryState.Modified), order: 1, group: 'navigation' }], @@ -354,7 +354,7 @@ export class ChatEditingRemoveAllFilesAction extends EditingSessionAction { override async runEditingSessionAction(accessor: ServicesAccessor, editingSession: IChatEditingSession, chatWidget: IChatWidget, ...args: any[]): Promise { // Remove all files from working set const uris = [...editingSession.entries.get()].map((e) => e.modifiedURI); - editingSession.remove(WorkingSetEntryRemovalReason.User, ...uris); + editingSession.remove(...uris); // Remove all file attachments const fileAttachments = chatWidget.attachmentModel ? chatWidget.attachmentModel.fileAttachments : []; @@ -687,3 +687,32 @@ registerAction2(class ResolveSymbolsContextAction extends EditingSessionAction { return implementations.flat(); } }); + +export class ViewPreviousEditsAction extends EditingSessionAction { + static readonly Id = 'chatEditing.viewPreviousEdits'; + static readonly Label = localize('chatEditing.viewPreviousEdits', 'View Previous Edits'); + + constructor() { + super({ + id: ViewPreviousEditsAction.Id, + title: ViewPreviousEditsAction.Label, + tooltip: ViewPreviousEditsAction.Label, + f1: false, + icon: Codicon.diffMultiple, + precondition: hasUndecidedChatEditingResourceContextKey.negate(), + menu: [ + { + id: MenuId.ChatEditingWidgetToolbar, + group: 'navigation', + order: 4, + when: ContextKeyExpr.and(applyingChatEditsFailedContextKey.negate(), ContextKeyExpr.and(hasAppliedChatEditsContextKey, hasUndecidedChatEditingResourceContextKey.negate())) + } + ], + }); + } + + override async runEditingSessionAction(accessor: ServicesAccessor, editingSession: IChatEditingSession, chatWidget: IChatWidget, ...args: any[]): Promise { + await editingSession.show(true); + } +} +registerAction2(ViewPreviousEditsAction); diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingCodeEditorIntegration.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingCodeEditorIntegration.ts index f79bbfd8842c..04be6a3d9d4f 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingCodeEditorIntegration.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingCodeEditorIntegration.ts @@ -36,7 +36,7 @@ import { EditorsOrder, IEditorIdentifier, isDiffEditorInput } from '../../../../ import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { overviewRulerModifiedForeground, minimapGutterModifiedBackground, overviewRulerAddedForeground, minimapGutterAddedBackground, overviewRulerDeletedForeground, minimapGutterDeletedBackground } from '../../../scm/common/quickDiff.js'; import { IChatAgentService } from '../../common/chatAgents.js'; -import { IModifiedFileEntry, IModifiedFileEntryChangeHunk, IModifiedFileEntryEditorIntegration, WorkingSetEntryState } from '../../common/chatEditingService.js'; +import { IModifiedFileEntry, IModifiedFileEntryChangeHunk, IModifiedFileEntryEditorIntegration, ModifiedFileEntryState } from '../../common/chatEditingService.js'; import { isTextDiffEditorForEntry } from './chatEditing.js'; import { IEditorDecorationsCollection } from '../../../../../editor/common/editorCommon.js'; import { ChatAgentLocation } from '../../common/constants.js'; @@ -642,7 +642,7 @@ export class ChatEditingCodeEditorIntegration implements IModifiedFileEntryEdito // close diff editor when entry is decided const d = autorun(r => { const state = this._entry.state.read(r); - if (state === WorkingSetEntryState.Accepted || state === WorkingSetEntryState.Rejected) { + if (state === ModifiedFileEntryState.Accepted || state === ModifiedFileEntryState.Rejected) { d.dispose(); const editorIdents: IEditorIdentifier[] = []; diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorActions.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorActions.ts index b9f1c37b8520..d207ac76f99f 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorActions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorActions.ts @@ -13,7 +13,7 @@ import { ctxHasEditorModification, ctxHasRequestInProgress, ctxReviewModeEnabled import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; import { EditorContextKeys } from '../../../../../editor/common/editorContextKeys.js'; import { ACTIVE_GROUP, IEditorService } from '../../../../services/editor/common/editorService.js'; -import { CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME, IChatEditingService, IChatEditingSession, IModifiedFileEntry, IModifiedFileEntryEditorIntegration, WorkingSetEntryState } from '../../common/chatEditingService.js'; +import { CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME, IChatEditingService, IChatEditingSession, IModifiedFileEntry, IModifiedFileEntryEditorIntegration, ModifiedFileEntryState } from '../../common/chatEditingService.js'; import { resolveCommandsContext } from '../../../../browser/parts/editor/editorCommandsContext.js'; import { IListService } from '../../../../../platform/list/browser/listService.js'; import { IEditorGroupsService } from '../../../../services/editor/common/editorGroupsService.js'; @@ -128,7 +128,7 @@ async function openNextOrPreviousChange(accessor: ServicesAccessor, session: ICh while (true) { idx = (idx + (next ? 1 : -1) + entries.length) % entries.length; newEntry = entries[idx]; - if (newEntry.state.get() === WorkingSetEntryState.Modified) { + if (newEntry.state.get() === ModifiedFileEntryState.Modified) { break; } else if (newEntry === entry) { return false; diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorContextKeys.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorContextKeys.ts index d86957b3ca83..5755dadb1753 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorContextKeys.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorContextKeys.ts @@ -13,7 +13,7 @@ import { IWorkbenchContribution } from '../../../../common/contributions.js'; import { EditorResourceAccessor, SideBySideEditor } from '../../../../common/editor.js'; import { IEditorGroup, IEditorGroupsService } from '../../../../services/editor/common/editorGroupsService.js'; import { IInlineChatSessionService } from '../../../inlineChat/browser/inlineChatSessionService.js'; -import { IChatEditingService, IChatEditingSession, IModifiedFileEntry, WorkingSetEntryState } from '../../common/chatEditingService.js'; +import { IChatEditingService, IChatEditingSession, IModifiedFileEntry, ModifiedFileEntryState } from '../../common/chatEditingService.js'; import { IChatService } from '../../common/chatService.js'; export const ctxIsGlobalEditingSession = new RawContextKey('chatEdits.isGlobalEditingSession', undefined, localize('chat.ctxEditSessionIsGlobal', "The current editor is part of the global edit session")); @@ -117,7 +117,7 @@ class ContextKeyGroup { ? observableFromEvent(this, chatModel.onDidChange, () => chatModel.requestInProgress) : constObservable(false); - this._ctxHasEditorModification.set(isInlineChat || entry?.state.read(r) === WorkingSetEntryState.Modified); + this._ctxHasEditorModification.set(isInlineChat || entry?.state.read(r) === ModifiedFileEntryState.Modified); this._ctxIsGlobalEditingSession.set(session.isGlobalEditingSession); this._ctxReviewModeEnabled.set(entry ? entry.reviewMode.read(r) : false); this._ctxHasRequestInProgress.set( diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorOverlay.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorOverlay.ts index 2f50e60efc7c..394530bffbc2 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorOverlay.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorOverlay.ts @@ -8,7 +8,7 @@ import { combinedDisposable, DisposableMap, DisposableStore, MutableDisposable, import { autorun, derived, derivedOpts, IObservable, observableFromEvent, observableSignalFromEvent, observableValue, transaction } from '../../../../../base/common/observable.js'; import { HiddenItemStrategy, MenuWorkbenchToolBar, WorkbenchToolBar } from '../../../../../platform/actions/browser/toolbar.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; -import { IChatEditingService, IChatEditingSession, IModifiedFileEntry, WorkingSetEntryState } from '../../common/chatEditingService.js'; +import { IChatEditingService, IChatEditingSession, IModifiedFileEntry, ModifiedFileEntryState } from '../../common/chatEditingService.js'; import { MenuId } from '../../../../../platform/actions/common/actions.js'; import { ActionViewItem } from '../../../../../base/browser/ui/actionbar/actionViewItems.js'; import { IActionRunner } from '../../../../../base/common/actions.js'; @@ -406,7 +406,7 @@ class ChatEditingOverlayController { const { session, entry } = data; if ( - entry?.state.read(r) === WorkingSetEntryState.Modified // any entry changing + entry?.state.read(r) === ModifiedFileEntryState.Modified // any entry changing || (!session.isGlobalEditingSession && isInProgress.read(r)) // inline chat request ) { // any session with changes diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedDocumentEntry.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedDocumentEntry.ts index 48a0038e7394..876e96165ba6 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedDocumentEntry.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedDocumentEntry.ts @@ -38,9 +38,9 @@ import { editorSelectionBackground } from '../../../../../platform/theme/common/ import { IUndoRedoElement, IUndoRedoService } from '../../../../../platform/undoRedo/common/undoRedo.js'; import { SaveReason, IEditorPane } from '../../../../common/editor.js'; import { IFilesConfigurationService } from '../../../../services/filesConfiguration/common/filesConfigurationService.js'; -import { IResolvedTextFileEditorModel, ITextFileService, stringToSnapshot } from '../../../../services/textfile/common/textfiles.js'; +import { isTextFileEditorModel, ITextFileService, stringToSnapshot } from '../../../../services/textfile/common/textfiles.js'; import { ICellEditOperation } from '../../../notebook/common/notebookCommon.js'; -import { IModifiedFileEntry, ChatEditKind, WorkingSetEntryState, IModifiedFileEntryEditorIntegration } from '../../common/chatEditingService.js'; +import { IModifiedFileEntry, ChatEditKind, ModifiedFileEntryState, IModifiedFileEntryEditorIntegration } from '../../common/chatEditingService.js'; import { IChatResponseModel } from '../../common/chatModel.js'; import { IChatService } from '../../common/chatService.js'; import { ChatEditingCodeEditorIntegration, IDocumentDiff2 } from './chatEditingCodeEditorIntegration.js'; @@ -76,7 +76,7 @@ export class ChatEditingModifiedDocumentEntry extends AbstractChatEditingModifie private readonly originalModel: ITextModel; private readonly modifiedModel: ITextModel; - readonly docFileEditorModel: IResolvedTextFileEditorModel; + private readonly _docFileEditorModel: IResolvedTextEditorModel; private _edit: OffsetEdit = OffsetEdit.empty; private _isEditFromUs: boolean = false; @@ -128,7 +128,7 @@ export class ChatEditingModifiedDocumentEntry extends AbstractChatEditingModifie instantiationService ); - this.docFileEditorModel = this._register(resourceRef).object as IResolvedTextFileEditorModel; + this._docFileEditorModel = this._register(resourceRef).object; this.modifiedModel = resourceRef.object.textEditorModel; this.originalURI = ChatEditingTextModelContentProvider.getFileURI(telemetryInfo.sessionId, this.entryId, this.modifiedURI.path); @@ -273,9 +273,9 @@ export class ChatEditingModifiedDocumentEntry extends AbstractChatEditingModifie const didResetToOriginalContent = this.modifiedModel.getValue() === this.initialContent; const currentState = this._stateObs.get(); switch (currentState) { - case WorkingSetEntryState.Modified: + case ModifiedFileEntryState.Modified: if (didResetToOriginalContent) { - this._stateObs.set(WorkingSetEntryState.Rejected, undefined); + this._stateObs.set(ModifiedFileEntryState.Rejected, undefined); break; } } @@ -319,7 +319,7 @@ export class ChatEditingModifiedDocumentEntry extends AbstractChatEditingModifie transaction((tx) => { if (!isLastEdits) { - this._stateObs.set(WorkingSetEntryState.Modified, tx); + this._stateObs.set(ModifiedFileEntryState.Modified, tx); this._isCurrentlyBeingModifiedByObs.set(responseModel, tx); const lineCount = this.modifiedModel.getLineCount(); this._rewriteRatioObs.set(Math.min(1, maxLineNumber / lineCount), tx); @@ -346,7 +346,7 @@ export class ChatEditingModifiedDocumentEntry extends AbstractChatEditingModifie this.originalModel.pushEditOperations(null, edits, _ => null); await this._updateDiffInfoSeq(); if (this._diffInfo.get().identical) { - this._stateObs.set(WorkingSetEntryState.Accepted, undefined); + this._stateObs.set(ModifiedFileEntryState.Accepted, undefined); } this._accessibilitySignalService.playSignal(AccessibilitySignal.editsKept, { allowManyInParallel: true }); return true; @@ -364,7 +364,7 @@ export class ChatEditingModifiedDocumentEntry extends AbstractChatEditingModifie this.modifiedModel.pushEditOperations(null, edits, _ => null); await this._updateDiffInfoSeq(); if (this._diffInfo.get().identical) { - this._stateObs.set(WorkingSetEntryState.Rejected, undefined); + this._stateObs.set(ModifiedFileEntryState.Rejected, undefined); } this._accessibilitySignalService.playSignal(AccessibilitySignal.editsUndone, { allowManyInParallel: true }); return true; @@ -451,15 +451,17 @@ export class ChatEditingModifiedDocumentEntry extends AbstractChatEditingModifie protected override async _doReject(tx: ITransaction | undefined): Promise { if (this.createdInRequestId === this._telemetryInfo.requestId) { - await this.docFileEditorModel.revert({ soft: true }); - await this._fileService.del(this.modifiedURI); + if (isTextFileEditorModel(this._docFileEditorModel)) { + await this._docFileEditorModel.revert({ soft: true }); + await this._fileService.del(this.modifiedURI); + } this._onDidDelete.fire(); } else { this._setDocValue(this.originalModel.getValue()); - if (this._allEditsAreFromUs) { + if (this._allEditsAreFromUs && isTextFileEditorModel(this._docFileEditorModel)) { // save the file after discarding so that the dirty indicator goes away // and so that an intermediate saved state gets reverted - await this.docFileEditorModel.save({ reason: SaveReason.EXPLICIT, skipSaveParticipants: true }); + await this._docFileEditorModel.save({ reason: SaveReason.EXPLICIT, skipSaveParticipants: true }); } await this._collapse(tx); } diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts index eb68edac8ff5..44cfbfa8293a 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts @@ -22,7 +22,7 @@ import { IEditorPane } from '../../../../common/editor.js'; import { IFilesConfigurationService } from '../../../../services/filesConfiguration/common/filesConfigurationService.js'; import { ICellEditOperation } from '../../../notebook/common/notebookCommon.js'; import { IChatAgentResult } from '../../common/chatAgents.js'; -import { ChatEditKind, IModifiedFileEntry, IModifiedFileEntryEditorIntegration, WorkingSetEntryState } from '../../common/chatEditingService.js'; +import { ChatEditKind, IModifiedFileEntry, IModifiedFileEntryEditorIntegration, ModifiedFileEntryState } from '../../common/chatEditingService.js'; import { IChatResponseModel } from '../../common/chatModel.js'; import { IChatService } from '../../common/chatService.js'; @@ -50,8 +50,8 @@ export abstract class AbstractChatEditingModifiedFileEntry extends Disposable im protected readonly _onDidDelete = this._register(new Emitter()); readonly onDidDelete = this._onDidDelete.event; - protected readonly _stateObs = observableValue(this, WorkingSetEntryState.Attached); - readonly state: IObservable = this._stateObs; + protected readonly _stateObs = observableValue(this, ModifiedFileEntryState.Modified); + readonly state: IObservable = this._stateObs; protected readonly _isCurrentlyBeingModifiedByObs = observableValue(this, undefined); readonly isCurrentlyBeingModifiedBy: IObservable = this._isCurrentlyBeingModifiedByObs; @@ -146,7 +146,7 @@ export abstract class AbstractChatEditingModifiedFileEntry extends Disposable im const cleanup = autorun(r => { // reset config when settled - const resetConfig = this.state.read(r) !== WorkingSetEntryState.Modified; + const resetConfig = this.state.read(r) !== ModifiedFileEntryState.Modified; if (resetConfig) { this._store.delete(cleanup); this._reviewModeTempObs.set(undefined, undefined); @@ -161,13 +161,13 @@ export abstract class AbstractChatEditingModifiedFileEntry extends Disposable im } async accept(tx: ITransaction | undefined): Promise { - if (this._stateObs.get() !== WorkingSetEntryState.Modified) { + if (this._stateObs.get() !== ModifiedFileEntryState.Modified) { // already accepted or rejected return; } await this._doAccept(tx); - this._stateObs.set(WorkingSetEntryState.Accepted, tx); + this._stateObs.set(ModifiedFileEntryState.Accepted, tx); this._autoAcceptCtrl.set(undefined, tx); this._notifyAction('accepted'); @@ -176,13 +176,13 @@ export abstract class AbstractChatEditingModifiedFileEntry extends Disposable im protected abstract _doAccept(tx: ITransaction | undefined): Promise; async reject(tx: ITransaction | undefined): Promise { - if (this._stateObs.get() !== WorkingSetEntryState.Modified) { + if (this._stateObs.get() !== ModifiedFileEntryState.Modified) { // already accepted or rejected return; } await this._doReject(tx); - this._stateObs.set(WorkingSetEntryState.Rejected, tx); + this._stateObs.set(ModifiedFileEntryState.Rejected, tx); this._autoAcceptCtrl.set(undefined, tx); this._notifyAction('rejected'); } @@ -309,6 +309,6 @@ export interface ISnapshotEntry { readonly original: string; readonly current: string; readonly originalToCurrentEdit: OffsetEdit; - readonly state: WorkingSetEntryState; + 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 a2065c8bdec3..adc7260476a1 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts @@ -43,7 +43,7 @@ import { INotebookEditorModelResolverService } from '../../../notebook/common/no import { INotebookLoggingService } from '../../../notebook/common/notebookLoggingService.js'; import { INotebookService } from '../../../notebook/common/notebookService.js'; import { INotebookEditorWorkerService } from '../../../notebook/common/services/notebookWorkerService.js'; -import { ChatEditKind, IModifiedFileEntryEditorIntegration, WorkingSetEntryState } from '../../common/chatEditingService.js'; +import { ChatEditKind, IModifiedFileEntryEditorIntegration, ModifiedFileEntryState } from '../../common/chatEditingService.js'; import { IChatResponseModel } from '../../common/chatModel.js'; import { IChatService } from '../../common/chatService.js'; import { AbstractChatEditingModifiedFileEntry, IModifiedEntryTelemetryInfo, ISnapshotEntry } from './chatEditingModifiedFileEntry.js'; @@ -115,7 +115,12 @@ export class ChatEditingModifiedNotebookEntry extends AbstractChatEditingModifie disposables.add(ChatEditingNotebookFileSystemProvider.registerFile(originalUri, buffer)); const originalRef = await resolver.resolve(originalUri, notebook.viewType); if (initialContent) { - restoreSnapshot(originalRef.object.notebook, initialContent); + try { + restoreSnapshot(originalRef.object.notebook, initialContent); + } catch (ex) { + console.error(`Error restoring snapshot: ${initialContent}`, ex); + initialContent = createSnapshot(notebook, options.serializer.options, configurationServie); + } } else { initialContent = createSnapshot(notebook, options.serializer.options, configurationServie); // Both models are the same, ensure the cell ids are the same, this way we get a perfect diffing. @@ -253,12 +258,13 @@ export class ChatEditingModifiedNotebookEntry extends AbstractChatEditingModifie // const didResetToOriginalContent = createSnapshot(this.modifiedModel, this.transientOptions, this.configurationService) === this.initialContent; let didResetToOriginalContent = this.initialContentComparer.isEqual(this.modifiedModel); const currentState = this._stateObs.get(); - if (currentState === WorkingSetEntryState.Rejected) { + if (currentState === ModifiedFileEntryState.Rejected) { return; } - if (currentState === WorkingSetEntryState.Modified && didResetToOriginalContent) { - this._stateObs.set(WorkingSetEntryState.Rejected, undefined); + if (currentState === ModifiedFileEntryState.Modified && didResetToOriginalContent) { + this._stateObs.set(ModifiedFileEntryState.Rejected, undefined); this.updateCellDiffInfo([], undefined); + this.initializeModelsFromDiff(); return; } @@ -394,9 +400,10 @@ export class ChatEditingModifiedNotebookEntry extends AbstractChatEditingModifie } didResetToOriginalContent = this.initialContentComparer.isEqual(this.modifiedModel); - if (currentState === WorkingSetEntryState.Modified && didResetToOriginalContent) { - this._stateObs.set(WorkingSetEntryState.Rejected, undefined); + if (currentState === ModifiedFileEntryState.Modified && didResetToOriginalContent) { + this._stateObs.set(ModifiedFileEntryState.Rejected, undefined); this.updateCellDiffInfo([], undefined); + this.initializeModelsFromDiff(); return; } } @@ -573,7 +580,7 @@ export class ChatEditingModifiedNotebookEntry extends AbstractChatEditingModifie transaction((tx) => { if (!isLastEdits) { - this._stateObs.set(WorkingSetEntryState.Modified, tx); + this._stateObs.set(ModifiedFileEntryState.Modified, tx); this._isCurrentlyBeingModifiedByObs.set(responseModel, tx); const newRewriteRation = Math.max(this._rewriteRatioObs.get(), calculateNotebookRewriteRatio(this._cellsDiffInfo.get(), this.originalModel, this.modifiedModel)); this._rewriteRatioObs.set(Math.min(1, newRewriteRation), tx); @@ -650,7 +657,7 @@ export class ChatEditingModifiedNotebookEntry extends AbstractChatEditingModifie private computeStateAfterAcceptingRejectingChanges(accepted: boolean) { const currentSnapshot = createSnapshot(this.modifiedModel, this.transientOptions, this.configurationService); if (new SnapshotComparer(currentSnapshot).isEqual(this.originalModel)) { - const state = accepted ? WorkingSetEntryState.Accepted : WorkingSetEntryState.Rejected; + const state = accepted ? ModifiedFileEntryState.Accepted : ModifiedFileEntryState.Rejected; this._stateObs.set(state, undefined); } } @@ -987,9 +994,9 @@ export class ChatEditingModifiedNotebookEntry extends AbstractChatEditingModifie } const cellState = cellEntry.state.read(r); - if (cellState === WorkingSetEntryState.Accepted) { + if (cellState === ModifiedFileEntryState.Accepted) { this.computeStateAfterAcceptingRejectingChanges(true); - } else if (cellState === WorkingSetEntryState.Rejected) { + } else if (cellState === ModifiedFileEntryState.Rejected) { this.computeStateAfterAcceptingRejectingChanges(false); } })); diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts index 498740b82a44..312addcafed4 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts @@ -35,7 +35,7 @@ import { IMultiDiffSourceResolver, IMultiDiffSourceResolverService, IResolvedMul import { CellUri } from '../../../notebook/common/notebookCommon.js'; import { INotebookService } from '../../../notebook/common/notebookService.js'; import { IChatAgentService } from '../../common/chatAgents.js'; -import { CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME, chatEditingAgentSupportsReadonlyReferencesContextKey, chatEditingResourceContextKey, ChatEditingSessionState, chatEditingSnapshotScheme, IChatEditingService, IChatEditingSession, IChatRelatedFile, IChatRelatedFilesProvider, IModifiedFileEntry, inChatEditingSessionContextKey, IStreamingEdits, WorkingSetEntryState } from '../../common/chatEditingService.js'; +import { CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME, chatEditingAgentSupportsReadonlyReferencesContextKey, chatEditingResourceContextKey, ChatEditingSessionState, chatEditingSnapshotScheme, IChatEditingService, IChatEditingSession, IChatRelatedFile, IChatRelatedFilesProvider, IModifiedFileEntry, inChatEditingSessionContextKey, IStreamingEdits, ModifiedFileEntryState, parseChatMultiDiffUri } from '../../common/chatEditingService.js'; import { ChatModel, IChatResponseModel, isCellTextEditOperation } from '../../common/chatModel.js'; import { IChatService } from '../../common/chatService.js'; import { ChatAgentLocation } from '../../common/constants.js'; @@ -394,7 +394,7 @@ class ChatDecorationsProvider extends Disposable implements IDecorationsProvider private readonly _modifiedUris = derived(this, (r) => { const uri = this._currentEntries.read(r); - return uri.filter(entry => !entry.isCurrentlyBeingModifiedBy.read(r) && entry.state.read(r) === WorkingSetEntryState.Modified).map(entry => entry.modifiedURI); + return uri.filter(entry => !entry.isCurrentlyBeingModifiedBy.read(r) && entry.state.read(r) === ModifiedFileEntryState.Modified).map(entry => entry.modifiedURI); }); public readonly onDidChange = Event.any( @@ -445,11 +445,12 @@ export class ChatEditingMultiDiffSourceResolver implements IMultiDiffSourceResol async resolveDiffSource(uri: URI): Promise { + const parsed = parseChatMultiDiffUri(uri); const thisSession = derived(this, r => { - return this._editingSessionsObs.read(r).find(candidate => candidate.chatSessionId === uri.authority); + return this._editingSessionsObs.read(r).find(candidate => candidate.chatSessionId === parsed.chatSessionId); }); - return this._instantiationService.createInstance(ChatEditingMultiDiffSource, thisSession); + return this._instantiationService.createInstance(ChatEditingMultiDiffSource, thisSession, parsed.showPreviousChanges); } } @@ -461,6 +462,21 @@ class ChatEditingMultiDiffSource implements IResolvedMultiDiffSource { } const entries = currentSession.entries.read(reader); return entries.map((entry) => { + if (this._showPreviousChanges) { + const entryDiffObs = currentSession.getEntryDiffBetweenStops(entry.modifiedURI, undefined, undefined); + const entryDiff = entryDiffObs?.read(reader); + if (entryDiff) { + return new MultiDiffEditorItem( + entryDiff.originalURI, + entryDiff.modifiedURI, + undefined, + { + [chatEditingResourceContextKey.key]: entry.entryId, + }, + ); + } + } + return new MultiDiffEditorItem( entry.originalURI, entry.modifiedURI, @@ -479,6 +495,7 @@ class ChatEditingMultiDiffSource implements IResolvedMultiDiffSource { }; constructor( - private readonly _currentSession: IObservable + private readonly _currentSession: IObservable, + private readonly _showPreviousChanges: boolean ) { } } diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts index 7bb3e9b91e4c..50662c8cbacc 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts @@ -38,7 +38,7 @@ import { IEditorService } from '../../../../services/editor/common/editorService import { MultiDiffEditor } from '../../../multiDiffEditor/browser/multiDiffEditor.js'; import { MultiDiffEditorInput } from '../../../multiDiffEditor/browser/multiDiffEditorInput.js'; import { INotebookService } from '../../../notebook/common/notebookService.js'; -import { ChatEditingSessionChangeType, ChatEditingSessionState, ChatEditKind, getMultiDiffSourceUri, IChatEditingSession, IEditSessionEntryDiff, IModifiedFileEntry, IStreamingEdits, WorkingSetDisplayMetadata, WorkingSetEntryRemovalReason, WorkingSetEntryState } from '../../common/chatEditingService.js'; +import { ChatEditingSessionState, ChatEditKind, getMultiDiffSourceUri, IChatEditingSession, IEditSessionEntryDiff, IModifiedFileEntry, IStreamingEdits, WorkingSetDisplayMetadata, ModifiedFileEntryState } from '../../common/chatEditingService.js'; import { IChatRequestDisablement, IChatResponseModel } from '../../common/chatModel.js'; import { IChatService } from '../../common/chatService.js'; import { AbstractChatEditingModifiedFileEntry, IModifiedEntryTelemetryInfo, ISnapshotEntry } from './chatEditingModifiedFileEntry.js'; @@ -121,6 +121,26 @@ function getCurrentAndNextStop(requestId: string, stopId: string | undefined, hi return { current, next }; } +function getFirstAndLastStop(uri: URI, history: readonly IChatEditingSessionSnapshot[]) { + let firstStopWithUri: IChatEditingSessionStop | undefined; + for (const snapshot of history) { + const stop = snapshot.stops.find(s => s.entries.has(uri)); + if (stop) { + firstStopWithUri = stop; + break; + } + } + const lastSnapshot = history.at(-1); + + const last = lastSnapshot?.postEdit ?? lastSnapshot?.stops.at(-1)?.entries; + + if (!firstStopWithUri || !last) { + return undefined; + } + + return { current: firstStopWithUri.entries, next: last }; +} + export class ChatEditingSession extends Disposable implements IChatEditingSession { private readonly _state = observableValue(this, ChatEditingSessionState.Initial); @@ -138,8 +158,6 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio return this._entriesObs; } - private _workingSet = new ResourceMap(); - private _editorPane: MultiDiffEditor | undefined; get state(): IObservable { @@ -168,12 +186,6 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio // return linearHistory.slice(linearHistoryIndex).map(s => s.requestId).filter((r): r is string => !!r); // }); - private readonly _onDidChange = this._register(new Emitter()); - get onDidChange() { - this._assertNotDisposed(); - return this._onDidChange.event; - } - private readonly _onDidDispose = new Emitter(); get onDidDispose() { this._assertNotDisposed(); @@ -198,6 +210,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio @IAccessibilitySignalService private readonly _accessibilitySignalService: IAccessibilitySignalService, ) { super(); + this._ignoreTrimWhitespaceObservable = observableConfigValue('diffEditor.ignoreTrimWhitespace', true, this._configurationService); } public async init(): Promise { @@ -213,6 +226,8 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio this._linearHistoryIndex.set(restoredSessionState.linearHistoryIndex, tx); this._state.set(ChatEditingSessionState.Idle, tx); }); + } else { + this._state.set(ChatEditingSessionState.Idle, undefined); } this._register(autorun(reader => { @@ -220,7 +235,6 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio entries.forEach(entry => { entry.state.read(reader); }); - this._onDidChange.fire(ChatEditingSessionChangeType.WorkingSet); })); } @@ -268,8 +282,9 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio } private _diffsBetweenStops = new Map>(); + private _fullDiffs = new Map>(); - private readonly _ignoreTrimWhitespaceObservable = observableConfigValue('diffEditor.ignoreTrimWhitespace', true, this._configurationService); + private readonly _ignoreTrimWhitespaceObservable: IObservable; /** * Gets diff for text entries between stops. @@ -298,7 +313,8 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio }); return derived((reader): ObservablePromise | undefined => { - const refs = modelRefsPromise.read(reader)?.promiseResult.read(reader)?.data; + const refs2 = modelRefsPromise.read(reader)?.promiseResult.read(reader); + const refs = refs2?.data; if (!refs) { return; } @@ -339,13 +355,15 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio }); } - private _createDiffBetweenStopsObservable(uri: URI, requestId: string, stopId: string | undefined): IObservable { + private _createDiffBetweenStopsObservable(uri: URI, requestId: string | undefined, stopId: string | undefined): IObservable { const entries = derivedOpts( { equalsFn: (a, b) => snapshotsEqualForDiff(a?.before, b?.before) && snapshotsEqualForDiff(a?.after, b?.after), }, reader => { - const stops = getCurrentAndNextStop(requestId, stopId, this._linearHistory.read(reader)); + const stops = requestId ? + getCurrentAndNextStop(requestId, stopId, this._linearHistory.read(reader)) : + getFirstAndLastStop(uri, this._linearHistory.read(reader)); if (!stops) { return undefined; } const before = stops.current.get(uri); const after = stops.next.get(uri); @@ -368,22 +386,30 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio }); } - public getEntryDiffBetweenStops(uri: URI, requestId: string, stopId: string | undefined) { - const key = `${uri}\0${requestId}\0${stopId}`; - let observable = this._diffsBetweenStops.get(key); - if (!observable) { - observable = this._createDiffBetweenStopsObservable(uri, requestId, stopId); - this._diffsBetweenStops.set(key, observable); - } + public getEntryDiffBetweenStops(uri: URI, requestId: string | undefined, stopId: string | undefined) { + if (requestId) { + const key = `${uri}\0${requestId}\0${stopId}`; + let observable = this._diffsBetweenStops.get(key); + if (!observable) { + observable = this._createDiffBetweenStopsObservable(uri, requestId, stopId); + this._diffsBetweenStops.set(key, observable); + } - return observable; + return observable; + } else { + const key = uri.toString(); + let observable = this._fullDiffs.get(key); + if (!observable) { + observable = this._createDiffBetweenStopsObservable(uri, requestId, stopId); + this._fullDiffs.set(key, observable); + } + + return observable; + } } public createSnapshot(requestId: string, undoStop: string | undefined): void { const snapshot = this._createSnapshot(requestId, undoStop); - for (const [uri, _] of this._workingSet) { - this._workingSet.set(uri, { state: WorkingSetEntryState.Sent }); - } const linearHistoryPtr = this._linearHistoryIndex.get(); const newLinearHistory: IChatEditingSessionSnapshot[] = []; @@ -410,7 +436,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio } private _createSnapshot(requestId: string | undefined, undoStop: string | undefined): IChatEditingSessionStop { - const workingSet = new ResourceMap(this._workingSet); + const entries = new ResourceMap(); for (const entry of this._entriesObs.get()) { entries.set(entry.modifiedURI, entry.createSnapshot(requestId, undoStop)); @@ -418,7 +444,6 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio return { stopId: undoStop, - workingSet, entries, }; } @@ -469,8 +494,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio } } - private async _restoreSnapshot({ workingSet, entries }: IChatEditingSessionStop, tx: ITransaction | undefined, restoreResolvedToDisk = true): Promise { - this._workingSet = new ResourceMap(workingSet); + private async _restoreSnapshot({ entries }: IChatEditingSessionStop, tx: ITransaction | undefined, restoreResolvedToDisk = true): Promise { // Reset all the files which are modified in this session state // but which are not found in the snapshot @@ -486,7 +510,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio // Restore all entries from the snapshot for (const snapshotEntry of entries.values()) { const entry = await this._getOrCreateModifiedFileEntry(snapshotEntry.resource, snapshotEntry.telemetryInfo); - const restoreToDisk = snapshotEntry.state === WorkingSetEntryState.Modified || restoreResolvedToDisk; + const restoreToDisk = snapshotEntry.state === ModifiedFileEntryState.Modified || restoreResolvedToDisk; entry.restoreFromSnapshot(snapshotEntry, restoreToDisk); entriesArr.push(entry); } @@ -494,7 +518,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio this._entriesObs.set(entriesArr, tx); } - remove(reason: WorkingSetEntryRemovalReason, ...uris: URI[]): void { + remove(...uris: URI[]): void { this._assertNotDisposed(); let didRemoveUris = false; @@ -508,17 +532,12 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio didRemoveUris = true; } - const state = this._workingSet.get(uri); - if (state !== undefined) { - didRemoveUris = this._workingSet.delete(uri) || didRemoveUris; - } } if (!didRemoveUris) { return; // noop } - this._onDidChange.fire(ChatEditingSessionChangeType.WorkingSet); } private _assertNotDisposed(): void { @@ -544,7 +563,6 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio } }); this._accessibilitySignalService.playSignal(AccessibilitySignal.editsKept, { allowManyInParallel: true }); - this._onDidChange.fire(ChatEditingSessionChangeType.Other); } async reject(...uris: URI[]): Promise { @@ -563,10 +581,9 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio } }); this._accessibilitySignalService.playSignal(AccessibilitySignal.editsUndone, { allowManyInParallel: true }); - this._onDidChange.fire(ChatEditingSessionChangeType.Other); } - async show(): Promise { + async show(previousChanges?: boolean): Promise { this._assertNotDisposed(); if (this._editorPane) { if (this._editorPane.isVisible()) { @@ -577,7 +594,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio } } const input = MultiDiffEditorInput.fromResourceMultiDiffEditorInput({ - multiDiffSource: getMultiDiffSourceUri(this), + multiDiffSource: getMultiDiffSourceUri(this, previousChanges), label: localize('multiDiffEditorInput.name', "Suggested Edits") }, this._instantiationService); @@ -795,7 +812,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio // special case: put the last change in the pendingSnapshot as needed if (next) { if (stopIndex === snap.stops.length - 1) { - const postEdit = new ResourceMap(snap.postEdit || this._createSnapshot(undefined, undefined).entries); + const postEdit = new ResourceMap(snap.postEdit || this._createSnapshot(requestId, undefined).entries); if (!snap.postEdit || !entry.equalsSnapshot(postEdit.get(entry.modifiedURI))) { postEdit.set(entry.modifiedURI, entry.createSnapshot(requestId, POST_EDIT_STOP_ID)); const newHistory = history.slice(); @@ -824,6 +841,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio } private async _acceptEdits(resource: URI, textEdits: (TextEdit | ICellEditOperation)[], isLastEdits: boolean, responseModel: IChatResponseModel): Promise { + this._fullDiffs.delete(resource.toString()); const entry = await this._getOrCreateModifiedFileEntry(resource, this._getTelemetryInfoForModel(responseModel)); await entry.acceptAgentEdits(resource, textEdits, isLastEdits, responseModel); } @@ -855,7 +873,6 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio return entry.acceptStreamingEditsEnd(tx); }); - this._onDidChange.fire(ChatEditingSessionChangeType.Other); } /** @@ -894,7 +911,6 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio const listener = entry.onDidDelete(() => { const newEntries = this._entriesObs.get().filter(e => !isEqual(e.modifiedURI, entry.modifiedURI)); this._entriesObs.set(newEntries, undefined); - this._workingSet.delete(entry.modifiedURI); this._editorService.closeEditors(this._editorService.findEditors(entry.modifiedURI)); if (!existingExternalEntry) { @@ -903,13 +919,11 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio } this._store.delete(listener); - this._onDidChange.fire(ChatEditingSessionChangeType.WorkingSet); }); this._store.add(listener); const entriesArr = [...this._entriesObs.get(), entry]; this._entriesObs.set(entriesArr, undefined); - this._onDidChange.fire(ChatEditingSessionChangeType.WorkingSet); return entry; } @@ -919,8 +933,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio const chatKind = mustExist ? ChatEditKind.Created : ChatEditKind.Modified; const notebookUri = CellUri.parse(resource)?.notebook || resource; try { - // If a notebook isn't open, then use the old synchronization approach. - if (this._notebookService.hasSupportedNotebooks(notebookUri) && (this._notebookService.getNotebookTextModel(notebookUri) || ChatEditingModifiedNotebookEntry.canHandleSnapshotContent(initialContent))) { + if (this._notebookService.hasSupportedNotebooks(notebookUri)) { return await ChatEditingModifiedNotebookEntry.create(notebookUri, multiDiffEntryDelegate, telemetryInfo, chatKind, initialContent, this._instantiationService); } else { const ref = await this._textModelService.createModelReference(resource); @@ -985,12 +998,6 @@ class ChatEditingSessionStorage { } return readPromise; }; - const deserializeResourceMap = (resourceMap: ResourceMapDTO, deserialize: (value: any) => T, result: ResourceMap): ResourceMap => { - resourceMap.forEach(([resourceURI, value]) => { - result.set(URI.parse(resourceURI), deserialize(value)); - }); - return result; - }; const deserializeSnapshotEntriesDTO = async (dtoEntries: ISnapshotEntryDTO[]): Promise> => { const entries = new ResourceMap(); for (const entryDTO of dtoEntries) { @@ -1001,14 +1008,13 @@ class ChatEditingSessionStorage { }; const deserializeChatEditingStopDTO = async (stopDTO: IChatEditingSessionStopDTO | IChatEditingSessionSnapshotDTO): Promise => { const entries = await deserializeSnapshotEntriesDTO(stopDTO.entries); - const workingSet = deserializeResourceMap(stopDTO.workingSet, (value) => value, new ResourceMap()); - return { stopId: 'stopId' in stopDTO ? stopDTO.stopId : undefined, workingSet, entries }; + return { stopId: 'stopId' in stopDTO ? stopDTO.stopId : undefined, entries }; }; const normalizeSnapshotDtos = (snapshot: IChatEditingSessionSnapshotDTO | IChatEditingSessionSnapshotDTO2): IChatEditingSessionSnapshotDTO2 => { if ('stops' in snapshot) { return snapshot; } - return { requestId: snapshot.requestId, stops: [{ stopId: undefined, entries: snapshot.entries, workingSet: snapshot.workingSet }], postEdit: undefined }; + return { requestId: snapshot.requestId, stops: [{ stopId: undefined, entries: snapshot.entries }], postEdit: undefined }; }; const deserializeChatEditingSessionSnapshot = async (startIndex: number, snapshot: IChatEditingSessionSnapshotDTO2): Promise => { const stops = await Promise.all(snapshot.stops.map(deserializeChatEditingStopDTO)); @@ -1104,7 +1110,6 @@ class ChatEditingSessionStorage { const serializeChatEditingSessionStop = (stop: IChatEditingSessionStop): IChatEditingSessionStopDTO => { return { stopId: stop.stopId, - workingSet: serializeResourceMap(stop.workingSet, value => value), entries: Array.from(stop.entries.values()).map(serializeSnapshotEntry) }; }; @@ -1190,13 +1195,11 @@ interface IChatEditingSessionStop { /** Edit stop ID, first for a request is always undefined. */ stopId: string | undefined; - readonly workingSet: ResourceMap; readonly entries: ResourceMap; } interface IChatEditingSessionStopDTO { readonly stopId: string | undefined; - readonly workingSet: ResourceMapDTO; readonly entries: ISnapshotEntryDTO[]; } @@ -1219,7 +1222,7 @@ interface ISnapshotEntryDTO { readonly originalHash: string; readonly currentHash: string; readonly originalToCurrentEdit: IOffsetEdit; - readonly state: WorkingSetEntryState; + 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 9418dc7c6bd0..7bf2577ec599 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingNotebookCellEntry.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingNotebookCellEntry.ts @@ -26,7 +26,7 @@ import { editorSelectionBackground } from '../../../../../../platform/theme/comm import { CellEditState } from '../../../../notebook/browser/notebookBrowser.js'; import { INotebookEditorService } from '../../../../notebook/browser/services/notebookEditorService.js'; import { NotebookCellTextModel } from '../../../../notebook/common/model/notebookCellTextModel.js'; -import { WorkingSetEntryState } from '../../../common/chatEditingService.js'; +import { ModifiedFileEntryState } from '../../../common/chatEditingService.js'; import { IChatResponseModel } from '../../../common/chatModel.js'; import { pendingRewriteMinimap } from '../chatEditingModifiedFileEntry.js'; @@ -83,8 +83,8 @@ export class ChatEditingNotebookCellEntry extends ObservableDisposable { private _editDecorations: string[] = []; private readonly _diffTrimWhitespace: IObservable; - protected readonly _stateObs = observableValue(this, WorkingSetEntryState.Modified); - readonly state: IObservable = this._stateObs; + protected readonly _stateObs = observableValue(this, ModifiedFileEntryState.Modified); + readonly state: IObservable = this._stateObs; protected readonly _isCurrentlyBeingModifiedByObs = observableValue(this, undefined); readonly isCurrentlyBeingModifiedBy: IObservable = this._isCurrentlyBeingModifiedByObs; private readonly initialContent: string; @@ -169,9 +169,9 @@ export class ChatEditingNotebookCellEntry extends ObservableDisposable { const didResetToOriginalContent = this.modifiedModel.getValue() === this.initialContent; const currentState = this._stateObs.get(); switch (currentState) { - case WorkingSetEntryState.Modified: + case ModifiedFileEntryState.Modified: if (didResetToOriginalContent) { - this._stateObs.set(WorkingSetEntryState.Rejected, undefined); + this._stateObs.set(ModifiedFileEntryState.Rejected, undefined); break; } } @@ -212,7 +212,7 @@ export class ChatEditingNotebookCellEntry extends ObservableDisposable { transaction((tx) => { if (!isLastEdits) { - this._stateObs.set(WorkingSetEntryState.Modified, tx); + this._stateObs.set(ModifiedFileEntryState.Modified, tx); this._isCurrentlyBeingModifiedByObs.set(responseModel, tx); this._maxModifiedLineNumber.set(maxLineNumber, tx); @@ -257,7 +257,7 @@ export class ChatEditingNotebookCellEntry extends ObservableDisposable { } await this._updateDiffInfoSeq(); if (this._diffInfo.get().identical) { - this._stateObs.set(WorkingSetEntryState.Accepted, undefined); + this._stateObs.set(ModifiedFileEntryState.Accepted, undefined); } return true; } @@ -283,7 +283,7 @@ export class ChatEditingNotebookCellEntry extends ObservableDisposable { } await this._updateDiffInfoSeq(); if (this._diffInfo.get().identical) { - this._stateObs.set(WorkingSetEntryState.Rejected, undefined); + this._stateObs.set(ModifiedFileEntryState.Rejected, undefined); } return true; } diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingNotebookEditorIntegration.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingNotebookEditorIntegration.ts index ce7a5d976049..21f150ba94f0 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingNotebookEditorIntegration.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingNotebookEditorIntegration.ts @@ -4,8 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable, IDisposable, toDisposable } from '../../../../../../base/common/lifecycle.js'; -import { autorun, IObservable, ISettableObservable, observableFromEvent, observableValue } from '../../../../../../base/common/observable.js'; -import { debouncedObservable } from '../../../../../../base/common/observableInternal/utils.js'; +import { autorun, debouncedObservable, IObservable, ISettableObservable, observableFromEvent, observableValue } from '../../../../../../base/common/observable.js'; import { basename } from '../../../../../../base/common/resources.js'; import { assertType } from '../../../../../../base/common/types.js'; import { LineRange } from '../../../../../../editor/common/core/lineRange.js'; diff --git a/src/vs/workbench/contrib/chat/browser/chatInlineAnchorWidget.ts b/src/vs/workbench/contrib/chat/browser/chatInlineAnchorWidget.ts index 3752bff4c6be..32c733f179b5 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInlineAnchorWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInlineAnchorWidget.ts @@ -365,11 +365,12 @@ registerAction2(class GoToDefinitionAction extends Action2 { override async run(accessor: ServicesAccessor, location: Location): Promise { const editorService = accessor.get(ICodeEditorService); + const instantiationService = accessor.get(IInstantiationService); await openEditorWithSelection(editorService, location); const action = new DefinitionAction({ openToSide: false, openInPeek: false, muteMessage: true }, { title: { value: '', original: '' }, id: '', precondition: undefined }); - return action.run(accessor); + return instantiationService.invokeFunction(accessor => action.run(accessor)); } }); diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 25c89c79e704..b98b41d8ee69 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -92,7 +92,7 @@ import { DefaultChatAttachmentWidget, FileAttachmentWidget, ImageAttachmentWidge import { IDisposableReference } from './chatContentParts/chatCollections.js'; import { CollapsibleListPool, IChatCollapsibleListItem } from './chatContentParts/chatReferencesContentPart.js'; import { ChatDragAndDrop } from './chatDragAndDrop.js'; -import { ChatEditingRemoveAllFilesAction, ChatEditingShowChangesAction } from './chatEditing/chatEditingActions.js'; +import { ChatEditingRemoveAllFilesAction, ChatEditingShowChangesAction, ViewPreviousEditsAction } from './chatEditing/chatEditingActions.js'; import { ChatFollowups } from './chatFollowups.js'; import { ChatSelectedTools } from './chatSelectedTools.js'; import { IChatViewState } from './chatWidget.js'; @@ -410,6 +410,13 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.setChatMode(ChatMode.Edit); } })); + + this._register(this.onDidChangeCurrentChatMode(() => this.accessibilityService.alert(this._currentMode))); + this._register(this._onDidChangeCurrentLanguageModel.event(() => { + if (this._currentLanguageModel?.metadata.name) { + this.accessibilityService.alert(this._currentLanguageModel.metadata.name); + } + })); } private getSelectedModelStorageKey(): string { @@ -468,7 +475,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge return; } - mode = validateChatMode(mode) ?? ChatMode.Ask; + mode = validateChatMode(mode) ?? (this.location === ChatAgentLocation.Panel ? ChatMode.Ask : ChatMode.Edit); if (mode === ChatMode.Agent && !this.agentService.hasToolsAgent) { mode = ChatMode.Edit; } @@ -1179,7 +1186,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge arg: { sessionId: chatEditingSession.chatSessionId }, }, buttonConfigProvider: (action) => { - if (action.id === ChatEditingShowChangesAction.ID || action.id === ChatEditingRemoveAllFilesAction.ID) { + if (action.id === ChatEditingShowChangesAction.ID || action.id === ChatEditingRemoveAllFilesAction.ID || action.id === ViewPreviousEditsAction.Id) { return { showIcon: true, showLabel: false, isSecondary: true }; } return undefined; diff --git a/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts index 604883bf16bb..e794b9ee88ff 100644 --- a/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts @@ -11,26 +11,35 @@ import { MarkdownString } from '../../../../base/common/htmlContent.js'; import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; import { Disposable, DisposableMap, DisposableStore } from '../../../../base/common/lifecycle.js'; import * as strings from '../../../../base/common/strings.js'; +import { URI } from '../../../../base/common/uri.js'; +import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js'; import { localize, localize2 } from '../../../../nls.js'; -import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { CommandsRegistry } from '../../../../platform/commands/common/commands.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; import { ExtensionIdentifier, IExtensionManifest } from '../../../../platform/extensions/common/extensions.js'; import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; import { ILogService } from '../../../../platform/log/common/log.js'; +import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; +import { ViewPane } from '../../../browser/parts/views/viewPane.js'; import { ViewPaneContainer } from '../../../browser/parts/views/viewPaneContainer.js'; -import { IWorkbenchContribution } from '../../../common/contributions.js'; +import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; import { IViewContainersRegistry, IViewDescriptor, IViewsRegistry, ViewContainer, ViewContainerLocation, Extensions as ViewExtensions } from '../../../common/views.js'; import { Extensions, IExtensionFeaturesRegistry, IExtensionFeatureTableRenderer, IRenderedData, IRowData, ITableData } from '../../../services/extensionManagement/common/extensionFeatures.js'; import { isProposedApiEnabled } from '../../../services/extensions/common/extensions.js'; import * as extensionsRegistry from '../../../services/extensions/common/extensionsRegistry.js'; +import { IViewsService } from '../../../services/views/common/viewsService.js'; import { showExtensionsWithIdsCommandId } from '../../extensions/browser/extensionsActions.js'; import { IExtension, IExtensionsWorkbenchService } from '../../extensions/common/extensions.js'; import { IChatAgentData, IChatAgentService } from '../common/chatAgents.js'; import { ChatContextKeys } from '../common/chatContextKeys.js'; import { IRawChatParticipantContribution } from '../common/chatParticipantContribTypes.js'; +import { IChatService } from '../common/chatService.js'; import { ChatAgentLocation, ChatConfiguration } from '../common/constants.js'; -import { ChatViewId } from './chat.js'; +import { ChatViewId, showChatView } from './chat.js'; import { CHAT_EDITING_SIDEBAR_PANEL_ID, CHAT_SIDEBAR_PANEL_ID, ChatViewPane } from './chatViewPane.js'; // --- Chat Container & View Registration @@ -87,38 +96,6 @@ const editsViewContainer: ViewContainer = Registry.as(V order: 101, }, ViewContainerLocation.AuxiliaryBar, { doNotRegisterOpenCommand: true }); -const editsViewDescriptor: IViewDescriptor[] = [{ - id: 'workbench.panel.chat.view.edits', - containerIcon: editsViewContainer.icon, - containerTitle: editsViewContainer.title.value, - singleViewPaneContainerTitle: editsViewContainer.title.value, - name: editsViewContainer.title, - canToggleVisibility: false, - canMoveView: true, - openCommandActionDescriptor: { - id: CHAT_EDITING_SIDEBAR_PANEL_ID, - title: editsViewContainer.title, - mnemonicTitle: localize({ key: 'miToggleEdits', comment: ['&& denotes a mnemonic'] }, "Copilot Ed&&its"), - keybindings: { - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyI, - linux: { - primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.KeyI - } - }, - order: 2 - }, - ctorDescriptor: new SyncDescriptor(ChatViewPane, [{ location: ChatAgentLocation.EditingSession }]), - when: ContextKeyExpr.and( - ContextKeyExpr.has(`config.${ChatConfiguration.UnifiedChatView}`).negate(), - ContextKeyExpr.or( - ChatContextKeys.Setup.hidden.negate(), - ChatContextKeys.Setup.installed, - ChatContextKeys.editingParticipantRegistered - ) - ) -}]; -Registry.as(ViewExtensions.ViewsRegistry).registerViews(editsViewDescriptor, editsViewContainer); - const chatParticipantExtensionPoint = extensionsRegistry.ExtensionsRegistry.registerExtensionPoint({ extensionPoint: 'chatParticipants', jsonSchema: { @@ -454,3 +431,143 @@ Registry.as(Extensions.ExtensionFeaturesRegistry).re }, renderer: new SyncDescriptor(ChatParticipantDataRenderer), }); + +// TODO@roblourens remove after a few months + +export class MovedChatEditsViewPane extends ViewPane { + override shouldShowWelcome(): boolean { + return true; + } +} + +const editsViewId = 'workbench.panel.chat.view.edits'; +const baseEditsViewDescriptor: IViewDescriptor = { + id: editsViewId, + containerIcon: editsViewContainer.icon, + containerTitle: editsViewContainer.title.value, + singleViewPaneContainerTitle: editsViewContainer.title.value, + name: editsViewContainer.title, + canToggleVisibility: false, + canMoveView: true, + openCommandActionDescriptor: { + id: CHAT_EDITING_SIDEBAR_PANEL_ID, + title: editsViewContainer.title, + mnemonicTitle: localize({ key: 'miToggleEdits', comment: ['&& denotes a mnemonic'] }, "Copilot Ed&&its"), + keybindings: { + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyI, + linux: { + primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.KeyI + } + }, + order: 2 + }, + ctorDescriptor: new SyncDescriptor(ChatViewPane, [{ location: ChatAgentLocation.EditingSession }]), + when: ContextKeyExpr.and( + ContextKeyExpr.has(`config.${ChatConfiguration.UnifiedChatView}`).negate(), + ContextKeyExpr.or( + ChatContextKeys.Setup.hidden.negate(), + ChatContextKeys.Setup.installed, + ChatContextKeys.editingParticipantRegistered + ) + ) +}; + +const ShowMovedChatEditsView = new RawContextKey('showMovedChatEditsView', true, { type: 'boolean', description: localize('hideMovedChatEditsView', "True when the moved chat edits view should be hidden.") }); + +class EditsViewContribution extends Disposable implements IWorkbenchContribution { + static readonly ID = 'workbench.contrib.chatEditsView'; + + private static readonly HideMovedEditsViewKey = 'chatEditsView.hideMovedEditsView'; + + private readonly showWelcomeViewCtx: IContextKey; + + constructor( + @IConfigurationService private readonly configurationService: IConfigurationService, + @IStorageService private readonly storageService: IStorageService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IChatService private readonly chatService: IChatService, + ) { + super(); + + this.showWelcomeViewCtx = ShowMovedChatEditsView.bindTo(this.contextKeyService); + + const unifiedViewEnabled = this.configurationService.getValue(ChatConfiguration.UnifiedChatView); + + const movedEditsViewDescriptor = { + ...baseEditsViewDescriptor, + ctorDescriptor: new SyncDescriptor(MovedChatEditsViewPane), + when: ContextKeyExpr.and( + ContextKeyExpr.has(`config.${ChatConfiguration.UnifiedChatView}`), + ShowMovedChatEditsView, + ContextKeyExpr.or( + ChatContextKeys.Setup.hidden.negate(), + ChatContextKeys.Setup.installed, + ChatContextKeys.editingParticipantRegistered + ) + ) + }; + + const editsViewToRegister = unifiedViewEnabled ? + movedEditsViewDescriptor : baseEditsViewDescriptor; + + if (unifiedViewEnabled) { + this.init(); + this.updateContextKey(); + this.registerWelcomeView(); + this.registerCommands(); + } + Registry.as(ViewExtensions.ViewsRegistry).registerViews([editsViewToRegister], editsViewContainer); + } + + private registerWelcomeView(): void { + const welcomeViewMainMessage = localize('editsMovedMainMessage', "Copilot Edits has been moved to the [main Chat view](command:workbench.action.chat.open). You can switch between modes by using the dropdown in the Chat input box."); + const okButton = `[${localize('ok', "Got it")}](command:_movedEditsView.ok)`; + const welcomeViewFooterMessage = localize('editsMovedFooterMessage', "[Learn more](command:_movedEditsView.learnMore) about the Chat view."); + + const viewsRegistry = Registry.as(ViewExtensions.ViewsRegistry); + this._register(viewsRegistry.registerViewWelcomeContent(editsViewId, { + content: [welcomeViewMainMessage, okButton, welcomeViewFooterMessage].join('\n\n'), + renderSecondaryButtons: true, + when: ShowMovedChatEditsView + })); + } + + private markViewToHide(): void { + this.storageService.store(EditsViewContribution.HideMovedEditsViewKey, true, StorageScope.APPLICATION, StorageTarget.USER); + this.updateContextKey(); + } + + private init() { + const hasChats = this.chatService.hasSessions(); + if (!hasChats) { + // No chats from previous sessions, might be a new user, so hide the view. + // Could also be a previous user who happened to first open a workspace with no chats. + this.markViewToHide(); + } + } + + private updateContextKey(): void { + const hidden = this.storageService.getBoolean(EditsViewContribution.HideMovedEditsViewKey, StorageScope.APPLICATION, false); + const hasChats = this.chatService.hasSessions(); + this.showWelcomeViewCtx.set(!hidden && hasChats); + } + + private registerCommands(): void { + this._register(CommandsRegistry.registerCommand({ + id: '_movedEditsView.ok', + handler: async (accessor: ServicesAccessor) => { + showChatView(accessor.get(IViewsService)); + this.markViewToHide(); + } + })); + this._register(CommandsRegistry.registerCommand({ + id: '_movedEditsView.learnMore', + handler: async (accessor: ServicesAccessor) => { + const openerService = accessor.get(IOpenerService); + openerService.open(URI.parse('https://aka.ms/vscode-chat-modes')); + } + })); + } +} + +registerWorkbenchContribution2(EditsViewContribution.ID, EditsViewContribution, WorkbenchPhase.BlockRestore); diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.ts index 70704840ca23..3d6ec2830cce 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -179,6 +179,8 @@ class SetupChatAgentImplementation extends Disposable implements IChatAgentImple private readonly _onUnresolvableError = this._register(new Emitter()); readonly onUnresolvableError = this._onUnresolvableError.event; + private readonly pendingForwardedRequests = new Map>(); + constructor( private readonly context: ChatEntitlementContext, private readonly controller: Lazy, @@ -227,14 +229,22 @@ class SetupChatAgentImplementation extends Disposable implements IChatAgentImple return {}; } - private _handlingForwardedRequest: string | undefined; private async forwardRequestToCopilot(requestModel: IChatRequestModel, progress: (part: IChatProgress) => void, chatService: IChatService, languageModelsService: ILanguageModelsService, chatAgentService: IChatAgentService, chatWidgetService: IChatWidgetService): Promise { - - if (this._handlingForwardedRequest === requestModel.message.text) { - throw new Error('Already handling this request'); + if (this.pendingForwardedRequests.has(requestModel.id)) { + return this.pendingForwardedRequests.get(requestModel.id)!; } - this._handlingForwardedRequest = requestModel.message.text; + const forwardRequest = this.doForwardRequestToCopilot(requestModel, progress, chatService, languageModelsService, chatAgentService, chatWidgetService); + this.pendingForwardedRequests.set(requestModel.id, forwardRequest); + + try { + await forwardRequest; + } finally { + this.pendingForwardedRequests.delete(requestModel.id); + } + } + + private async doForwardRequestToCopilot(requestModel: IChatRequestModel, progress: (part: IChatProgress) => void, chatService: IChatService, languageModelsService: ILanguageModelsService, chatAgentService: IChatAgentService, chatWidgetService: IChatWidgetService): Promise { // We need a signal to know when we can resend the request to // Copilot. Waiting for the registration of the agent is not @@ -254,13 +264,17 @@ class SetupChatAgentImplementation extends Disposable implements IChatAgentImple try { const ready = await Promise.race([ timeout(20000).then(() => 'timedout'), + this.whenDefaultAgentFailed(chatService).then(() => 'error'), Promise.allSettled([whenLanguageModelReady, whenAgentReady]) ]); - if (ready === 'timedout') { + if (ready === 'error' || ready === 'timedout') { progress({ kind: 'warning', - content: new MarkdownString(localize('copilotTookLongWarning', "Copilot took too long to get ready. Please try again.")) + content: new MarkdownString(ready === 'timedout' ? + localize('copilotTookLongWarning', "Copilot took too long to get ready. Please try again.") : + localize('copilotFailedWarning', "Copilot failed to get ready. Please try again.") + ) }); // This means Copilot is unhealthy and we cannot retry the @@ -303,6 +317,12 @@ class SetupChatAgentImplementation extends Disposable implements IChatAgentImple })); } + private async whenDefaultAgentFailed(chatService: IChatService): Promise { + return new Promise(resolve => { + chatService.activateDefaultAgent(this.location).catch(() => resolve()); + }); + } + private async doInvokeWithSetup(request: IChatAgentRequest, progress: (part: IChatProgress) => void, chatService: IChatService, languageModelsService: ILanguageModelsService, chatWidgetService: IChatWidgetService, chatAgentService: IChatAgentService): Promise { this.telemetryService.publicLog2('workbenchActionExecuted', { id: CHAT_SETUP_ACTION_ID, from: 'chat' }); diff --git a/src/vs/workbench/contrib/chat/browser/chatStatus.ts b/src/vs/workbench/contrib/chat/browser/chatStatus.ts index 3ef14731ee12..fea9a9352d27 100644 --- a/src/vs/workbench/contrib/chat/browser/chatStatus.ts +++ b/src/vs/workbench/contrib/chat/browser/chatStatus.ts @@ -29,7 +29,7 @@ import { ILanguageService } from '../../../../editor/common/languages/language.j import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { Button } from '../../../../base/browser/ui/button/button.js'; import { renderLabelWithIcons } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; -import { WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification } from '../../../../base/common/actions.js'; +import { WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification, IAction, toAction } from '../../../../base/common/actions.js'; import { parseLinkedText } from '../../../../base/common/linkedText.js'; import { Link } from '../../../../platform/opener/browser/link.js'; import { IOpenerService } from '../../../../platform/opener/common/opener.js'; @@ -37,6 +37,11 @@ import { ITelemetryService } from '../../../../platform/telemetry/common/telemet import { IChatStatusItemService, ChatStatusEntry } from './chatStatusItemService.js'; import { ITextResourceConfigurationService } from '../../../../editor/common/services/textResourceConfiguration.js'; import { EditorResourceAccessor, SideBySideEditor } from '../../../common/editor.js'; +import { getCodeEditor } from '../../../../editor/browser/editorBrowser.js'; +import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js'; +import { ThemeIcon } from '../../../../base/common/themables.js'; +import { Codicon } from '../../../../base/common/codicons.js'; +import { URI } from '../../../../base/common/uri.js'; const gaugeBackground = registerColor('gauge.background', { dark: inputValidationInfoBorder, @@ -92,7 +97,8 @@ registerColor('gauge.errorForeground', { const defaultChat = { extensionId: product.defaultChatAgent?.extensionId ?? '', completionsEnablementSetting: product.defaultChatAgent?.completionsEnablementSetting ?? '', - nextEditSuggestionsSetting: product.defaultChatAgent?.nextEditSuggestionsSetting ?? '' + nextEditSuggestionsSetting: product.defaultChatAgent?.nextEditSuggestionsSetting ?? '', + manageSettingsUrl: product.defaultChatAgent?.manageSettingsUrl ?? '', }; export class ChatStatusBarEntry extends Disposable implements IWorkbenchContribution { @@ -103,6 +109,8 @@ export class ChatStatusBarEntry extends Disposable implements IWorkbenchContribu private dashboard = new Lazy(() => this.instantiationService.createInstance(ChatStatusDashboard)); + private readonly activeCodeEditorListener = this._register(new MutableDisposable()); + constructor( @IChatEntitlementService private readonly chatEntitlementService: ChatEntitlementService, @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -136,7 +144,9 @@ export class ChatStatusBarEntry extends Disposable implements IWorkbenchContribu this._register(this.chatEntitlementService.onDidChangeQuotaExceeded(() => this.entry?.update(this.getEntryProps()))); this._register(this.chatEntitlementService.onDidChangeSentiment(() => this.entry?.update(this.getEntryProps()))); this._register(this.chatEntitlementService.onDidChangeEntitlement(() => this.entry?.update(this.getEntryProps()))); - this._register(this.editorService.onDidActiveEditorChange(() => this.entry?.update(this.getEntryProps()))); + + this._register(this.editorService.onDidActiveEditorChange(() => this.onDidActiveEditorChange())); + this._register(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(defaultChat.completionsEnablementSetting)) { this.entry?.update(this.getEntryProps()); @@ -144,6 +154,20 @@ export class ChatStatusBarEntry extends Disposable implements IWorkbenchContribu })); } + private onDidActiveEditorChange(): void { + this.entry?.update(this.getEntryProps()); + + this.activeCodeEditorListener.clear(); + + // Listen to language changes in the active code editor + const activeCodeEditor = getCodeEditor(this.editorService.activeTextEditorControl); + if (activeCodeEditor) { + this.activeCodeEditorListener.value = activeCodeEditor.onDidChangeModelLanguage(() => { + this.entry?.update(this.getEntryProps()); + }); + } + } + private getEntryProps(): IStatusbarEntry { let text = '$(copilot)'; let ariaLabel = localize('chatStatus', "Copilot Status"); @@ -179,7 +203,7 @@ export class ChatStatusBarEntry extends Disposable implements IWorkbenchContribu // Completions Disabled else if (this.editorService.activeTextEditorLanguageId && !isCompletionsEnabled(this.configurationService, this.editorService.activeTextEditorLanguageId)) { - text = `$(copilot-not-connected)`; + text = `$(copilot-unavailable)`; ariaLabel = localize('completionsDisabledStatus', "Code Completions Disabled"); } } @@ -263,14 +287,18 @@ class ChatStatusDashboard extends Disposable { disposables.add(token.onCancellationRequested(() => disposables.dispose())); let needsSeparator = false; - const addSeparator = (label: string | undefined) => { + const addSeparator = (label?: string, action?: IAction) => { if (needsSeparator) { this.element.appendChild($('hr')); - needsSeparator = false; } - if (label) { - this.element.appendChild($('div.header', undefined, label)); + if (label || action) { + const header = this.element.appendChild($('div.header', undefined, label ?? '')); + + if (action) { + const toolbar = disposables.add(new ActionBar(header)); + toolbar.push([action], { icon: true, label: false }); + } } needsSeparator = true; @@ -280,7 +308,13 @@ class ChatStatusDashboard extends Disposable { if (this.chatEntitlementService.entitlement === ChatEntitlement.Limited) { const { chatTotal, chatRemaining, completionsTotal, completionsRemaining, quotaResetDate, chatQuotaExceeded, completionsQuotaExceeded } = this.chatEntitlementService.quotas; - addSeparator(localize('usageTitle', "Copilot Free Plan Usage")); + addSeparator(localize('usageTitle', "Copilot Free Plan Usage"), toAction({ + id: 'workbench.action.openChatSettings', + label: localize('quotaLabel', "Manage Copilot"), + tooltip: localize('quotaTooltip', "Manage Copilot"), + class: ThemeIcon.asClassName(Codicon.settings), + run: () => this.runCommandAndClose(() => this.openerService.open(URI.parse(defaultChat.manageSettingsUrl))), + })); const chatQuotaIndicator = this.createQuotaIndicator(this.element, chatTotal, chatRemaining, localize('chatsLabel', "Chat messages")); const completionsQuotaIndicator = this.createQuotaIndicator(this.element, completionsTotal, completionsRemaining, localize('completionsLabel', "Code completions")); @@ -309,21 +343,22 @@ class ChatStatusDashboard extends Disposable { // Contributions { for (const item of this.chatStatusItemService.getEntries()) { - addSeparator(undefined); - const chatItemDisposables = disposables.add(new MutableDisposable()); + addSeparator(); + + const itemDisposables = disposables.add(new MutableDisposable()); let rendered = this.renderContributedChatStatusItem(item); - chatItemDisposables.value = rendered.disposables; + itemDisposables.value = rendered.disposables; this.element.appendChild(rendered.element); disposables.add(this.chatStatusItemService.onDidChange(e => { if (e.entry.id === item.id) { - const oldEl = rendered.element; + const previousElement = rendered.element; rendered = this.renderContributedChatStatusItem(e.entry); - chatItemDisposables.value = rendered.disposables; + itemDisposables.value = rendered.disposables; - oldEl.replaceWith(rendered.element); + previousElement.replaceWith(rendered.element); } })); } @@ -331,7 +366,13 @@ class ChatStatusDashboard extends Disposable { // Settings { - addSeparator(localize('settingsTitle', "Settings")); + addSeparator(localize('settingsTitle', "Settings"), this.chatEntitlementService.sentiment === ChatSentiment.Installed ? toAction({ + id: 'workbench.action.openChatSettings', + label: localize('settingsLabel', "Settings"), + tooltip: localize('settingsTooltip', "Open Settings"), + class: ThemeIcon.asClassName(Codicon.settingsGear), + run: () => this.runCommandAndClose(() => this.commandService.executeCommand('workbench.action.openSettings', { query: `@id:${defaultChat.completionsEnablementSetting} @id:${defaultChat.nextEditSuggestionsSetting}` })), + }) : undefined); this.createSettings(this.element, disposables); } @@ -341,7 +382,7 @@ class ChatStatusDashboard extends Disposable { const newUser = isNewUser(this.chatEntitlementService); const signedOut = this.chatEntitlementService.entitlement === ChatEntitlement.Unknown; if (newUser || signedOut) { - addSeparator(undefined); + 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."))); @@ -357,21 +398,21 @@ class ChatStatusDashboard extends Disposable { private renderContributedChatStatusItem(item: ChatStatusEntry): { element: HTMLElement; disposables: DisposableStore } { const disposables = new DisposableStore(); - const entryEl = $('div.contribution'); + const itemElement = $('div.contribution'); - entryEl.appendChild($('div.header', undefined, item.label)); + itemElement.appendChild($('div.header', undefined, item.label)); - const bodyEl = entryEl.appendChild($('div.body')); + const itemBody = itemElement.appendChild($('div.body')); - const descriptionEl = bodyEl.appendChild($('span.description')); - this.renderTextPlus(descriptionEl, item.description, disposables); + const description = itemBody.appendChild($('span.description')); + this.renderTextPlus(description, item.description, disposables); if (item.detail) { - const itemElement = bodyEl.appendChild($('div.detail-item')); - this.renderTextPlus(itemElement, item.detail, disposables); + const detail = itemBody.appendChild($('div.detail-item')); + this.renderTextPlus(detail, item.detail, disposables); } - return { element: entryEl, disposables }; + return { element: itemElement, disposables }; } private renderTextPlus(target: HTMLElement, text: string, store: DisposableStore): void { diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index 18ec857e08ad..0d87066959fa 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -41,7 +41,7 @@ import { IThemeService } from '../../../../platform/theme/common/themeService.js import { checkModeOption } from '../common/chat.js'; import { IChatAgentCommand, IChatAgentData, IChatAgentService, IChatWelcomeMessageContent, isChatWelcomeMessageContent } from '../common/chatAgents.js'; import { ChatContextKeys } from '../common/chatContextKeys.js'; -import { applyingChatEditsFailedContextKey, decidedChatEditingResourceContextKey, hasAppliedChatEditsContextKey, hasUndecidedChatEditingResourceContextKey, IChatEditingService, IChatEditingSession, inChatEditingSessionContextKey, WorkingSetEntryState } from '../common/chatEditingService.js'; +import { applyingChatEditsFailedContextKey, decidedChatEditingResourceContextKey, hasAppliedChatEditsContextKey, hasUndecidedChatEditingResourceContextKey, IChatEditingService, IChatEditingSession, inChatEditingSessionContextKey, ModifiedFileEntryState } from '../common/chatEditingService.js'; import { ChatPauseState, IChatModel, IChatRequestVariableEntry, IChatResponseModel } from '../common/chatModel.js'; import { chatAgentLeader, ChatRequestAgentPart, chatSubcommandLeader, formatChatQuestion, IParsedChatRequest } from '../common/chatParserTypes.js'; import { ChatRequestParser } from '../common/chatRequestParser.js'; @@ -276,13 +276,13 @@ export class ChatWidget extends Disposable implements IChatWidget { return; } const entries = currentSession.entries.read(reader); - const decidedEntries = entries.filter(entry => entry.state.read(reader) !== WorkingSetEntryState.Modified); + const decidedEntries = entries.filter(entry => entry.state.read(reader) !== ModifiedFileEntryState.Modified); return decidedEntries.map(entry => entry.entryId); })); this._register(bindContextKey(hasUndecidedChatEditingResourceContextKey, contextKeyService, (reader) => { const currentSession = this._editingSession.read(reader); const entries = currentSession?.entries.read(reader) ?? []; // using currentSession here - const decidedEntries = entries.filter(entry => entry.state.read(reader) === WorkingSetEntryState.Modified); + const decidedEntries = entries.filter(entry => entry.state.read(reader) === ModifiedFileEntryState.Modified); return decidedEntries.length > 0; })); this._register(bindContextKey(hasAppliedChatEditsContextKey, contextKeyService, (reader) => { @@ -334,11 +334,10 @@ export class ChatWidget extends Disposable implements IChatWidget { return; } + session.entries.read(r); // SIGNAL + this._editingSession.set(session, undefined); - store.add(session.onDidChange(() => { - this.renderChatEditingSessionState(); - })); store.add(session.onDidDispose(() => { this._editingSession.set(undefined, undefined); this.renderChatEditingSessionState(); diff --git a/src/vs/workbench/contrib/chat/browser/contrib/screenshot.ts b/src/vs/workbench/contrib/chat/browser/contrib/screenshot.ts index ca08338bb434..b050697d043f 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/screenshot.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/screenshot.ts @@ -3,16 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { VSBuffer } from '../../../../../base/common/buffer.js'; import { localize } from '../../../../../nls.js'; import { IChatRequestVariableEntry } from '../../common/chatModel.js'; export const ScreenshotVariableId = 'screenshot-focused-window'; -export function convertBufferToScreenshotVariable(buffer: ArrayBufferLike): IChatRequestVariableEntry { +export function convertBufferToScreenshotVariable(buffer: VSBuffer): IChatRequestVariableEntry { return { id: ScreenshotVariableId, name: localize('screenshot', 'Screenshot'), - value: new Uint8Array(buffer), + value: buffer.buffer, isImage: true, }; } diff --git a/src/vs/workbench/contrib/chat/browser/media/chatStatus.css b/src/vs/workbench/contrib/chat/browser/media/chatStatus.css index a8fe69420ba4..50ba77730bd3 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatStatus.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatStatus.css @@ -16,11 +16,17 @@ } .chat-status-bar-entry-tooltip div.header { + display: flex; + align-items: center; color: var(--vscode-descriptionForeground); margin-bottom: 4px; font-weight: 600; } +.chat-status-bar-entry-tooltip div.header .monaco-action-bar { + margin-left: auto; +} + .chat-status-bar-entry-tooltip div.description { color: var(--vscode-descriptionForeground); } diff --git a/src/vs/workbench/contrib/chat/common/chatEditingService.ts b/src/vs/workbench/contrib/chat/common/chatEditingService.ts index 0f1b2a97e758..bffdc01c99ec 100644 --- a/src/vs/workbench/contrib/chat/common/chatEditingService.ts +++ b/src/vs/workbench/contrib/chat/common/chatEditingService.ts @@ -65,7 +65,7 @@ export interface IChatRelatedFilesProvider { } export interface WorkingSetDisplayMetadata { - state: WorkingSetEntryState; + state: ModifiedFileEntryState; description?: string; } @@ -82,12 +82,11 @@ export const chatEditingSnapshotScheme = 'chat-editing-snapshot-text-model'; export interface IChatEditingSession extends IDisposable { readonly isGlobalEditingSession: boolean; readonly chatSessionId: string; - readonly onDidChange: Event; readonly onDidDispose: Event; readonly state: IObservable; readonly entries: IObservable; - show(): Promise; - remove(reason: WorkingSetEntryRemovalReason, ...uris: URI[]): void; + show(previousChanges?: boolean): Promise; + remove(...uris: URI[]): void; accept(...uris: URI[]): Promise; reject(...uris: URI[]): Promise; getEntry(uri: URI): IModifiedFileEntry | undefined; @@ -119,7 +118,7 @@ export interface IChatEditingSession extends IDisposable { * the next one. * @returns The observable or undefined if there is no diff between the stops. */ - getEntryDiffBetweenStops(uri: URI, requestId: string, stopId: string | undefined): IObservable | undefined; + getEntryDiffBetweenStops(uri: URI, requestId: string | undefined, stopId: string | undefined): IObservable | undefined; readonly canUndo: IObservable; readonly canRedo: IObservable; @@ -142,23 +141,10 @@ export interface IEditSessionEntryDiff { removed: number; } -export const enum WorkingSetEntryRemovalReason { - User, - Programmatic -} - -export const enum WorkingSetEntryState { +export const enum ModifiedFileEntryState { Modified, Accepted, Rejected, - Transient, // TODO@joyceerhl remove this - Attached, // TODO@joyceerhl remove this - Sent, // TODO@joyceerhl remove this -} - -export const enum ChatEditingSessionChangeType { - WorkingSet, - Other, } /** @@ -222,7 +208,7 @@ export interface IModifiedFileEntry { readonly lastModifyingRequestId: string; - readonly state: IObservable; + readonly state: IObservable; readonly isCurrentlyBeingModifiedBy: IObservable; readonly rewriteRatio: IObservable; @@ -255,7 +241,7 @@ export const enum ChatEditingSessionState { export const CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME = 'chat-editing-multi-diff-source'; -export const chatEditingWidgetFileStateContextKey = new RawContextKey('chatEditingWidgetFileState', undefined, localize('chatEditingWidgetFileState', "The current state of the file in the chat editing widget")); +export const chatEditingWidgetFileStateContextKey = new RawContextKey('chatEditingWidgetFileState', undefined, localize('chatEditingWidgetFileState', "The current state of the file in the chat editing widget")); export const chatEditingAgentSupportsReadonlyReferencesContextKey = new RawContextKey('chatEditingAgentSupportsReadonlyReferences', undefined, localize('chatEditingAgentSupportsReadonlyReferences', "Whether the chat editing agent supports readonly references (temporary)")); export const decidedChatEditingResourceContextKey = new RawContextKey('decidedChatEditingResource', []); export const chatEditingResourceContextKey = new RawContextKey('chatEditingResource', undefined); @@ -281,9 +267,17 @@ export function isChatEditingActionContext(thing: unknown): thing is IChatEditin return typeof thing === 'object' && !!thing && 'sessionId' in thing; } -export function getMultiDiffSourceUri(session: IChatEditingSession): URI { +export function getMultiDiffSourceUri(session: IChatEditingSession, showPreviousChanges?: boolean): URI { return URI.from({ scheme: CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME, authority: session.chatSessionId, + query: showPreviousChanges ? 'previous' : undefined, }); } + +export function parseChatMultiDiffUri(uri: URI): { chatSessionId: string; showPreviousChanges: boolean } { + const chatSessionId = uri.authority; + const showPreviousChanges = uri.query === 'previous'; + + return { chatSessionId, showPreviousChanges }; +} diff --git a/src/vs/workbench/contrib/chat/common/chatEntitlementService.ts b/src/vs/workbench/contrib/chat/common/chatEntitlementService.ts index a6c791b1a114..c2b5507f24c4 100644 --- a/src/vs/workbench/contrib/chat/common/chatEntitlementService.ts +++ b/src/vs/workbench/contrib/chat/common/chatEntitlementService.ts @@ -275,8 +275,7 @@ export class ChatEntitlementService extends Disposable implements IChatEntitleme //#region --- Sentiment - private readonly _onDidChangeSentiment = this._register(new Emitter()); - readonly onDidChangeSentiment = this._onDidChangeSentiment.event; + readonly onDidChangeSentiment: Event; get sentiment(): ChatSentiment { if (this.contextKeyService.getContextKeyValue(ChatContextKeys.Setup.installed.key) === true) { diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index 4ba63bfd57ea..654dbd2f607e 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -523,6 +523,8 @@ export interface IChatService { readonly unifiedViewEnabled: boolean; isEditingLocation(location: ChatAgentLocation): boolean; + + activateDefaultAgent(location: ChatAgentLocation): Promise; } export const KEYWORD_ACTIVIATION_SETTING_ID = 'accessibility.voice.keywordActivation'; diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index 5a734d53c256..e13d86ea7482 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -34,6 +34,7 @@ import { IChatCompleteResponse, IChatDetail, IChatFollowup, IChatProgress, IChat import { ChatServiceTelemetry } from './chatServiceTelemetry.js'; import { ChatSessionStore, IChatTransfer2 } from './chatSessionStore.js'; import { IChatSlashCommandService } from './chatSlashCommands.js'; +import { IChatTransferService } from './chatTransferService.js'; import { IChatVariablesService } from './chatVariables.js'; import { ChatAgentLocation, ChatConfiguration, ChatMode } from './constants.js'; import { ChatMessageRole, IChatMessage } from './languageModels.js'; @@ -158,6 +159,7 @@ export class ChatService extends Disposable implements IChatService { @IChatAgentService private readonly chatAgentService: IChatAgentService, @IConfigurationService private readonly configurationService: IConfigurationService, @IWorkbenchAssignmentService private readonly experimentService: IWorkbenchAssignmentService, + @IChatTransferService private readonly chatTransferService: IChatTransferService, ) { super(); @@ -490,17 +492,16 @@ export class ChatService extends Disposable implements IChatService { this.trace('initializeSession', `Initialize session ${model.sessionId}`); model.startInitialize(); - await this.extensionService.whenInstalledExtensionsRegistered(); - const defaultAgentData = this.chatAgentService.getContributedDefaultAgent(model.initialLocation) ?? this.chatAgentService.getContributedDefaultAgent(ChatAgentLocation.Panel); - if (!defaultAgentData) { - throw new ErrorNoTelemetry('No default agent contributed'); + const activation = this.activateDefaultAgent(model.initialLocation); + if (this.configurationService.getValue('chat.setupFromDialog')) { + // 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. + activation.catch(e => this.logService.error(e)); + } else { + await activation; } - // 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.extensionService.activateByEvent(`onChatParticipant:${defaultAgentData.id}`); - model.initialize(); } catch (err) { this.trace('startSession', `initializeSession failed: ${err}`); @@ -510,6 +511,23 @@ export class ChatService extends Disposable implements IChatService { } } + async activateDefaultAgent(location: ChatAgentLocation): Promise { + await this.extensionService.whenInstalledExtensionsRegistered(); + + const defaultAgentData = this.chatAgentService.getContributedDefaultAgent(location) ?? this.chatAgentService.getContributedDefaultAgent(ChatAgentLocation.Panel); + if (!defaultAgentData) { + throw new ErrorNoTelemetry('No default agent contributed'); + } + + // No setup participant to fall back on- wait for extension activation + await this.extensionService.activateByEvent(`onChatParticipant:${defaultAgentData.id}`); + + const defaultAgent = this.chatAgentService.getActivatedAgents().find(agent => agent.id === defaultAgentData.id); + if (!defaultAgent) { + throw new ErrorNoTelemetry('No default agent registered'); + } + } + getSession(sessionId: string): IChatModel | undefined { return this._sessionModels.get(sessionId); } @@ -1128,6 +1146,7 @@ export class ChatService extends Disposable implements IChatService { }); this.storageService.store(globalChatKey, JSON.stringify(existingRaw), StorageScope.PROFILE, StorageTarget.MACHINE); + this.chatTransferService.addWorkspaceToTransferred(toWorkspace); this.trace('transferChatSession', `Transferred session ${model.sessionId} to workspace ${toWorkspace.toString()}`); } diff --git a/src/vs/workbench/contrib/chat/common/chatSessionStore.ts b/src/vs/workbench/contrib/chat/common/chatSessionStore.ts index 5aa68347f65e..c73798e98ebc 100644 --- a/src/vs/workbench/contrib/chat/common/chatSessionStore.ts +++ b/src/vs/workbench/contrib/chat/common/chatSessionStore.ts @@ -224,19 +224,17 @@ export class ChatSessionStore extends Disposable { const fileOperationReason = error && toFileOperationResult(error); type ChatSessionStoreErrorData = { - reason: string; + errorReason: string; fileOperationReason: number; - // error: Error; }; type ChatSessionStoreErrorClassification = { owner: 'roblourens'; comment: 'Detect issues related to managing chat sessions'; - reason: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Info about the error that occurred' }; + errorReason: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Info about the error that occurred' }; fileOperationReason: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'An error code from the file service' }; - // error: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Info about the error that occurred' }; }; this.telemetryService.publicLog2('chatSessionStoreError', { - reason: reasonForTelemetry, + errorReason: reasonForTelemetry, fileOperationReason: fileOperationReason ?? -1 }); } diff --git a/src/vs/workbench/contrib/chat/common/chatTransferService.ts b/src/vs/workbench/contrib/chat/common/chatTransferService.ts index 22a2eb31aa85..bbc21070343a 100644 --- a/src/vs/workbench/contrib/chat/common/chatTransferService.ts +++ b/src/vs/workbench/contrib/chat/common/chatTransferService.ts @@ -3,18 +3,21 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; -import { IStorageService } from '../../../../platform/storage/common/storage.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { IFileService } from '../../../../platform/files/common/files.js'; import { IWorkspaceTrustManagementService } from '../../../../platform/workspace/common/workspaceTrust.js'; -import { isChatTransferredWorkspace, areWorkspaceFoldersEmpty } from '../../../services/workspaces/common/workspaceUtils.js'; +import { areWorkspaceFoldersEmpty } from '../../../services/workspaces/common/workspaceUtils.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; +import { URI } from '../../../../base/common/uri.js'; export const IChatTransferService = createDecorator('chatTransferService'); +const transferredWorkspacesKey = 'chat.transferedWorkspaces'; export interface IChatTransferService { readonly _serviceBrand: undefined; - checkAndSetWorkspaceTrust(): Promise; + checkAndSetTransferredWorkspaceTrust(): Promise; + addWorkspaceToTransferred(workspace: URI): void; } export class ChatTransferService implements IChatTransferService { @@ -27,10 +30,35 @@ export class ChatTransferService implements IChatTransferService { @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService ) { } - async checkAndSetWorkspaceTrust(): Promise { + deleteWorkspaceFromTransferredList(workspace: URI): void { + const transferredWorkspaces = this.storageService.getObject(transferredWorkspacesKey, StorageScope.PROFILE, []); + const updatedWorkspaces = transferredWorkspaces.filter(uri => uri !== workspace.toString()); + this.storageService.store(transferredWorkspacesKey, updatedWorkspaces, StorageScope.PROFILE, StorageTarget.MACHINE); + } + + addWorkspaceToTransferred(workspace: URI): void { + const transferredWorkspaces = this.storageService.getObject(transferredWorkspacesKey, StorageScope.PROFILE, []); + transferredWorkspaces.push(workspace.toString()); + this.storageService.store(transferredWorkspacesKey, transferredWorkspaces, StorageScope.PROFILE, StorageTarget.MACHINE); + } + + async checkAndSetTransferredWorkspaceTrust(): Promise { const workspace = this.workspaceService.getWorkspace(); - if (isChatTransferredWorkspace(workspace, this.storageService) && await areWorkspaceFoldersEmpty(workspace, this.fileService)) { + const currentWorkspaceUri = workspace.folders[0]?.uri; + if (!currentWorkspaceUri) { + return; + } + if (this.isChatTransferredWorkspace(currentWorkspaceUri, this.storageService) && await areWorkspaceFoldersEmpty(workspace, this.fileService)) { await this.workspaceTrustManagementService.setWorkspaceTrust(true); + this.deleteWorkspaceFromTransferredList(currentWorkspaceUri); } } + + isChatTransferredWorkspace(workspace: URI, storageService: IStorageService): boolean { + if (!workspace) { + return false; + } + const chatWorkspaceTransfer: URI[] = storageService.getObject(transferredWorkspacesKey, StorageScope.PROFILE, []); + return chatWorkspaceTransfer.some(item => item.toString() === workspace.toString()); + } } diff --git a/src/vs/workbench/contrib/chat/common/chatWidgetHistoryService.ts b/src/vs/workbench/contrib/chat/common/chatWidgetHistoryService.ts index 4522c0b7bd33..865425b70e27 100644 --- a/src/vs/workbench/contrib/chat/common/chatWidgetHistoryService.ts +++ b/src/vs/workbench/contrib/chat/common/chatWidgetHistoryService.ts @@ -8,7 +8,7 @@ import { URI } from '../../../../base/common/uri.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { Memento } from '../../../common/memento.js'; -import { WorkingSetEntryState } from './chatEditingService.js'; +import { ModifiedFileEntryState } from './chatEditingService.js'; import { IChatRequestVariableEntry } from './chatModel.js'; import { CHAT_PROVIDER_ID } from './chatParticipantContribTypes.js'; import { ChatAgentLocation, ChatMode } from './constants.js'; @@ -22,7 +22,7 @@ export interface IChatHistoryEntry { export interface IChatInputState { [key: string]: any; chatContextAttachments?: ReadonlyArray; - chatWorkingSet?: ReadonlyArray<{ uri: URI; state: WorkingSetEntryState }>; + chatWorkingSet?: ReadonlyArray<{ uri: URI; state: ModifiedFileEntryState }>; chatMode?: ChatMode; } diff --git a/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts b/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts index 7ce74db646ab..1099dca6e394 100644 --- a/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts +++ b/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts @@ -250,7 +250,8 @@ export class CodeBlockModelCollection extends Disposable { function fixCodeText(text: string, languageId: string | undefined): string { if (languageId === 'php') { - if (!text.trim().startsWith('<')) { + // 1 ? validUris : undefined + }; } async prepareToolInvocation(parameters: any, token: CancellationToken): Promise { diff --git a/src/vs/workbench/contrib/chat/test/browser/chatEditingService.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatEditingService.test.ts index f7612029ae71..6056df9b990c 100644 --- a/src/vs/workbench/contrib/chat/test/browser/chatEditingService.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/chatEditingService.test.ts @@ -33,6 +33,7 @@ import { INotebookService } from '../../../notebook/common/notebookService.js'; import { Range } from '../../../../../editor/common/core/range.js'; import { ChatAgentLocation } from '../../common/constants.js'; import { NotebookTextModel } from '../../../notebook/common/model/notebookTextModel.js'; +import { ChatTransferService, IChatTransferService } from '../../common/chatTransferService.js'; function getAgentData(id: string) { return { @@ -62,6 +63,7 @@ suite('ChatEditingService', function () { collection.set(IChatAgentService, new SyncDescriptor(ChatAgentService)); collection.set(IChatVariablesService, new MockChatVariablesService()); collection.set(IChatSlashCommandService, new class extends mock() { }); + collection.set(IChatTransferService, new SyncDescriptor(ChatTransferService)); collection.set(IChatEditingService, new SyncDescriptor(ChatEditingService)); collection.set(IChatService, new SyncDescriptor(ChatService)); collection.set(IMultiDiffSourceResolverService, new class extends mock() { diff --git a/src/vs/workbench/contrib/chat/test/common/mockChatService.ts b/src/vs/workbench/contrib/chat/test/common/mockChatService.ts index 57bc91080eda..1edbedf8bc14 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockChatService.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockChatService.ts @@ -107,4 +107,8 @@ export class MockChatService implements IChatService { isPersistedSessionEmpty(sessionId: string): boolean { throw new Error('Method not implemented.'); } + + activateDefaultAgent(location: ChatAgentLocation): Promise { + throw new Error('Method not implemented.'); + } } diff --git a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts index 972fd363b90a..37963ed5fbac 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.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 { localize, localize2 } from '../../../../../nls.js'; import { IKeyMods, IQuickInputService } from '../../../../../platform/quickinput/common/quickInput.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; @@ -22,7 +23,7 @@ import { IEditorGroupsService } from '../../../../services/editor/common/editorG export class GotoLineQuickAccessProvider extends AbstractGotoLineQuickAccessProvider { - protected readonly onDidActiveTextEditorControlChange = this.editorService.onDidActiveEditorChange; + protected readonly onDidActiveTextEditorControlChange: Event; constructor( @IEditorService private readonly editorService: IEditorService, @@ -30,6 +31,7 @@ export class GotoLineQuickAccessProvider extends AbstractGotoLineQuickAccessProv @IConfigurationService private readonly configurationService: IConfigurationService ) { super(); + this.onDidActiveTextEditorControlChange = this.editorService.onDidActiveEditorChange; } private get configuration() { diff --git a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts index 037908fa8db3..d8dea5ca723e 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.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 { localize, localize2 } from '../../../../../nls.js'; import { IKeyMods, IQuickPickSeparator, IQuickInputService, IQuickPick, ItemActivation } from '../../../../../platform/quickinput/common/quickInput.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; @@ -37,7 +38,7 @@ import { matchesFuzzyIconAware, parseLabelWithIcons } from '../../../../../base/ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccessProvider { - protected readonly onDidActiveTextEditorControlChange = this.editorService.onDidActiveEditorChange; + protected readonly onDidActiveTextEditorControlChange: Event; constructor( @IEditorService private readonly editorService: IEditorService, @@ -50,6 +51,7 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess super(languageFeaturesService, outlineModelService, { openSideBySideDirection: () => this.configuration.openSideBySideDirection }); + this.onDidActiveTextEditorControlChange = this.editorService.onDidActiveEditorChange; } //#region DocumentSymbols (text editor required) diff --git a/src/vs/workbench/contrib/debug/browser/debugMemory.ts b/src/vs/workbench/contrib/debug/browser/debugMemory.ts index 8fcd11962b30..9eabf24fed7c 100644 --- a/src/vs/workbench/contrib/debug/browser/debugMemory.ts +++ b/src/vs/workbench/contrib/debug/browser/debugMemory.ts @@ -235,11 +235,12 @@ class MemoryRegionView extends Disposable implements IMemoryRegion { public readonly onDidInvalidate = this.invalidateEmitter.event; public readonly writable: boolean; - private readonly width = this.range.toOffset - this.range.fromOffset; + private readonly width: number; constructor(private readonly parent: IMemoryRegion, public readonly range: { fromOffset: number; toOffset: number }) { super(); this.writable = parent.writable; + this.width = range.toOffset - range.fromOffset; this._register(parent); this._register(parent.onDidInvalidate(e => { diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index 3e63e4891679..7054f7ad505e 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -3,22 +3,26 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { getActiveWindow } from '../../../../base/browser/dom.js'; import * as aria from '../../../../base/browser/ui/aria/aria.js'; +import { mainWindow } from '../../../../base/browser/window.js'; import { distinct } from '../../../../base/common/arrays.js'; import { Queue, RunOnceScheduler, raceTimeout } from '../../../../base/common/async.js'; import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; import { canceled } from '../../../../base/common/errors.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { normalizeDriveLetter } from '../../../../base/common/labels.js'; -import { Disposable, DisposableMap, DisposableStore, IDisposable, MutableDisposable, dispose } from '../../../../base/common/lifecycle.js'; +import { Disposable, DisposableMap, DisposableStore, MutableDisposable, dispose } from '../../../../base/common/lifecycle.js'; import { mixin } from '../../../../base/common/objects.js'; import * as platform from '../../../../base/common/platform.js'; import * as resources from '../../../../base/common/resources.js'; import Severity from '../../../../base/common/severity.js'; +import { isDefined } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; import { generateUuid } from '../../../../base/common/uuid.js'; import { IPosition, Position } from '../../../../editor/common/core/position.js'; import { localize } from '../../../../nls.js'; +import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../../platform/log/common/log.js'; @@ -28,28 +32,24 @@ import { ICustomEndpointTelemetryService, ITelemetryService, TelemetryLevel } fr import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; import { IWorkspaceContextService, IWorkspaceFolder } from '../../../../platform/workspace/common/workspace.js'; import { ViewContainerLocation } from '../../../common/views.js'; -import { RawDebugSession } from './rawDebugSession.js'; +import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; +import { IHostService } from '../../../services/host/browser/host.js'; +import { ILifecycleService } from '../../../services/lifecycle/common/lifecycle.js'; +import { IPaneCompositePartService } from '../../../services/panecomposite/browser/panecomposite.js'; +import { LiveTestResult } from '../../testing/common/testResult.js'; +import { ITestResultService } from '../../testing/common/testResultService.js'; +import { ITestService } from '../../testing/common/testService.js'; import { AdapterEndEvent, IBreakpoint, IConfig, IDataBreakpoint, IDataBreakpointInfoResponse, IDebugConfiguration, IDebugLocationReferenced, IDebugService, IDebugSession, IDebugSessionOptions, IDebugger, IExceptionBreakpoint, IExceptionInfo, IFunctionBreakpoint, IInstructionBreakpoint, IMemoryRegion, IRawModelUpdate, IRawStoppedDetails, IReplElement, IStackFrame, IThread, LoadedSourceEvent, State, VIEWLET_ID, isFrameDeemphasized } from '../common/debug.js'; import { DebugCompoundRoot } from '../common/debugCompoundRoot.js'; import { DebugModel, ExpressionContainer, MemoryRegion, Thread } from '../common/debugModel.js'; import { Source } from '../common/debugSource.js'; import { filterExceptionsFromTelemetry } from '../common/debugUtils.js'; import { INewReplElementData, ReplModel } from '../common/replModel.js'; -import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; -import { IHostService } from '../../../services/host/browser/host.js'; -import { ILifecycleService } from '../../../services/lifecycle/common/lifecycle.js'; -import { IPaneCompositePartService } from '../../../services/panecomposite/browser/panecomposite.js'; -import { getActiveWindow } from '../../../../base/browser/dom.js'; -import { mainWindow } from '../../../../base/browser/window.js'; -import { isDefined } from '../../../../base/common/types.js'; -import { ITestService } from '../../testing/common/testService.js'; -import { ITestResultService } from '../../testing/common/testResultService.js'; -import { LiveTestResult } from '../../testing/common/testResult.js'; -import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js'; +import { RawDebugSession } from './rawDebugSession.js'; const TRIGGERED_BREAKPOINT_MAX_DELAY = 1500; -export class DebugSession implements IDebugSession, IDisposable { +export class DebugSession implements IDebugSession { parentSession: IDebugSession | undefined; rememberedCapabilities?: DebugProtocol.Capabilities; diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index 6ce5e3e38a93..545504bfde0e 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -368,7 +368,7 @@ export interface IDebugLocationReferenced { source: Source; } -export interface IDebugSession extends ITreeElement { +export interface IDebugSession extends ITreeElement, IDisposable { readonly configuration: IConfig; readonly unresolvedConfiguration: IConfig | undefined; diff --git a/src/vs/workbench/contrib/debug/common/debugModel.ts b/src/vs/workbench/contrib/debug/common/debugModel.ts index 75794f9dfd45..75062d56a1da 100644 --- a/src/vs/workbench/contrib/debug/common/debugModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugModel.ts @@ -744,10 +744,11 @@ export class MemoryRegion extends Disposable implements IMemoryRegion { public readonly onDidInvalidate = this.invalidateEmitter.event; /** @inheritdoc */ - public readonly writable = !!this.session.capabilities.supportsWriteMemoryRequest; + public readonly writable: boolean; constructor(private readonly memoryReference: string, private readonly session: IDebugSession) { super(); + this.writable = !!this.session.capabilities.supportsWriteMemoryRequest; this._register(session.onDidInvalidateMemory(e => { if (e.body.memoryReference === memoryReference) { this.invalidate(e.body.offset, e.body.count - e.body.offset); @@ -1489,6 +1490,7 @@ export class DebugModel extends Disposable implements IDebugModel { } if (s.state === State.Inactive && s.configuration.name === session.configuration.name) { // Make sure to remove all inactive sessions that are using the same configuration as the new session + s.dispose(); return false; } diff --git a/src/vs/workbench/contrib/debug/common/debugStorage.ts b/src/vs/workbench/contrib/debug/common/debugStorage.ts index 5c2a8f485bab..051b1bd0958e 100644 --- a/src/vs/workbench/contrib/debug/common/debugStorage.ts +++ b/src/vs/workbench/contrib/debug/common/debugStorage.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable } from '../../../../base/common/lifecycle.js'; -import { observableValue } from '../../../../base/common/observable.js'; +import { ISettableObservable, observableValue } from '../../../../base/common/observable.js'; import { URI } from '../../../../base/common/uri.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; @@ -28,11 +28,11 @@ export interface IChosenEnvironment { } export class DebugStorage extends Disposable { - public readonly breakpoints = observableValue(this, this.loadBreakpoints()); - public readonly functionBreakpoints = observableValue(this, this.loadFunctionBreakpoints()); - public readonly exceptionBreakpoints = observableValue(this, this.loadExceptionBreakpoints()); - public readonly dataBreakpoints = observableValue(this, this.loadDataBreakpoints()); - public readonly watchExpressions = observableValue(this, this.loadWatchExpressions()); + public readonly breakpoints: ISettableObservable; + public readonly functionBreakpoints: ISettableObservable; + public readonly exceptionBreakpoints: ISettableObservable; + public readonly dataBreakpoints: ISettableObservable; + public readonly watchExpressions: ISettableObservable; constructor( @IStorageService private readonly storageService: IStorageService, @@ -41,6 +41,11 @@ export class DebugStorage extends Disposable { @ILogService private readonly logService: ILogService ) { super(); + this.breakpoints = observableValue(this, this.loadBreakpoints()); + this.functionBreakpoints = observableValue(this, this.loadFunctionBreakpoints()); + this.exceptionBreakpoints = observableValue(this, this.loadExceptionBreakpoints()); + this.dataBreakpoints = observableValue(this, this.loadDataBreakpoints()); + this.watchExpressions = observableValue(this, this.loadWatchExpressions()); this._register(storageService.onDidChangeValue(StorageScope.WORKSPACE, undefined, this._store)(e => { if (e.external) { diff --git a/src/vs/workbench/contrib/debug/test/common/mockDebug.ts b/src/vs/workbench/contrib/debug/test/common/mockDebug.ts index 8d3c433daac5..4ce86561a407 100644 --- a/src/vs/workbench/contrib/debug/test/common/mockDebug.ts +++ b/src/vs/workbench/contrib/debug/test/common/mockDebug.ts @@ -179,6 +179,10 @@ export class MockSession implements IDebugSession { readonly suppressDebugView = false; readonly autoExpandLazyVariables = false; + dispose(): void { + + } + getMemory(memoryReference: string): IMemoryRegion { throw new Error('Method not implemented.'); } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index bcf99d677c20..7c7da9508a06 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -933,7 +933,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension private readonly _onReset = new Emitter(); get onReset() { return this._onReset.event; } - readonly preferPreReleases = this.productService.quality !== 'stable'; + readonly preferPreReleases: boolean; private installing: IExtension[] = []; private tasksInProgress: CancelablePromise[] = []; @@ -978,10 +978,8 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension @IAllowedExtensionsService private readonly allowedExtensionsService: IAllowedExtensionsService, ) { super(); - const preferPreReleasesValue = configurationService.getValue('_extensions.preferPreReleases'); - if (!isUndefined(preferPreReleasesValue)) { - this.preferPreReleases = !!preferPreReleasesValue; - } + this.preferPreReleases = productService.quality !== 'stable'; + this.hasOutdatedExtensionsContextKey = HasOutdatedExtensionsContext.bindTo(contextKeyService); if (extensionManagementServerService.localExtensionManagementServer) { this.localExtensions = this._register(instantiationService.createInstance(Extensions, diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 6ce571740018..0a1ae04748a3 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -53,7 +53,7 @@ import { EditorBasedInlineChatWidget } from './inlineChatWidget.js'; import { InlineChatZoneWidget } from './inlineChatZoneWidget.js'; import { ChatAgentLocation } from '../../chat/common/constants.js'; import { ChatContextKeys } from '../../chat/common/chatContextKeys.js'; -import { IChatEditingService, WorkingSetEntryState } from '../../chat/common/chatEditingService.js'; +import { IChatEditingService, ModifiedFileEntryState } from '../../chat/common/chatEditingService.js'; export const enum State { CREATE_SESSION = 'CREATE_SESSION', @@ -1403,7 +1403,7 @@ export class InlineChatController2 implements IEditorContribution { await Event.toPromise(session.editingSession.onDidDispose); - const rejected = session.editingSession.getEntry(uri)?.state.get() === WorkingSetEntryState.Rejected; + const rejected = session.editingSession.getEntry(uri)?.state.get() === ModifiedFileEntryState.Rejected; return !rejected; } @@ -1455,7 +1455,7 @@ export async function reviewEdits(accessor: ServicesAccessor, editor: ICodeEdito return false; } const state = entry.state.read(r); - return state === WorkingSetEntryState.Accepted || state === WorkingSetEntryState.Rejected; + return state === ModifiedFileEntryState.Accepted || state === ModifiedFileEntryState.Rejected; }); const whenDecided = waitForState(isSettled, Boolean); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts index 97b36199564f..acca853caacb 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts @@ -30,7 +30,7 @@ import { ITextFileService } from '../../../services/textfile/common/textfiles.js import { UntitledTextEditorInput } from '../../../services/untitled/common/untitledTextEditorInput.js'; import { IChatWidgetService } from '../../chat/browser/chat.js'; import { IChatAgentService } from '../../chat/common/chatAgents.js'; -import { WorkingSetEntryState } from '../../chat/common/chatEditingService.js'; +import { ModifiedFileEntryState } from '../../chat/common/chatEditingService.js'; import { IChatService } from '../../chat/common/chatService.js'; import { ChatAgentLocation } from '../../chat/common/constants.js'; import { CTX_INLINE_CHAT_HAS_AGENT, CTX_INLINE_CHAT_HAS_AGENT2, CTX_INLINE_CHAT_POSSIBLE } from '../common/inlineChat.js'; @@ -361,7 +361,7 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { const allSettled = entries.every(entry => { const state = entry.state.read(r); - return (state === WorkingSetEntryState.Accepted || state === WorkingSetEntryState.Rejected) + return (state === ModifiedFileEntryState.Accepted || state === ModifiedFileEntryState.Rejected) && !entry.isCurrentlyBeingModifiedBy.read(r); }); diff --git a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts index 5e2024222e7f..1efc0b9e2868 100644 --- a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts +++ b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts @@ -63,6 +63,7 @@ import { assertSnapshot } from '../../../../../base/test/common/snapshot.js'; import { IObservable, constObservable } from '../../../../../base/common/observable.js'; import { IChatEditingService, IChatEditingSession } from '../../../chat/common/chatEditingService.js'; import { ChatAgentLocation } from '../../../chat/common/constants.js'; +import { ChatTransferService, IChatTransferService } from '../../../chat/common/chatTransferService.js'; suite('InlineChatSession', function () { @@ -89,6 +90,7 @@ suite('InlineChatSession', function () { [IChatWidgetHistoryService, new SyncDescriptor(ChatWidgetHistoryService)], [IChatWidgetService, new SyncDescriptor(ChatWidgetService)], [IChatSlashCommandService, new SyncDescriptor(ChatSlashCommandService)], + [IChatTransferService, new SyncDescriptor(ChatTransferService)], [IChatService, new SyncDescriptor(ChatService)], [IEditorWorkerService, new SyncDescriptor(TestWorkerService)], [IChatAgentService, new SyncDescriptor(ChatAgentService)], diff --git a/src/vs/workbench/contrib/inlineCompletions/browser/inlineCompletionLanguageStatusBarContribution.ts b/src/vs/workbench/contrib/inlineCompletions/browser/inlineCompletionLanguageStatusBarContribution.ts index 75930b380dd8..586e2251675a 100644 --- a/src/vs/workbench/contrib/inlineCompletions/browser/inlineCompletionLanguageStatusBarContribution.ts +++ b/src/vs/workbench/contrib/inlineCompletions/browser/inlineCompletionLanguageStatusBarContribution.ts @@ -3,16 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize } from '../../../../nls.js'; import { createHotClass } from '../../../../base/common/hotReloadHelpers.js'; import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; -import { autorunWithStore, derived } from '../../../../base/common/observable.js'; -import { debouncedObservable } from '../../../../base/common/observableInternal/utils.js'; +import { autorunWithStore, debouncedObservable, derived } from '../../../../base/common/observable.js'; import Severity from '../../../../base/common/severity.js'; import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; -import { InlineCompletionsController } from '../../../../editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.js'; -import { ILanguageStatusService } from '../../../services/languageStatus/common/languageStatusService.js'; import { observableCodeEditor } from '../../../../editor/browser/observableCodeEditor.js'; +import { InlineCompletionsController } from '../../../../editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.js'; +import { localize } from '../../../../nls.js'; +import { ILanguageStatusService } from '../../../services/languageStatus/common/languageStatusService.js'; export class InlineCompletionLanguageStatusBarContribution extends Disposable { public static readonly hot = createHotClass(InlineCompletionLanguageStatusBarContribution); diff --git a/src/vs/workbench/contrib/markers/browser/markersViewActions.ts b/src/vs/workbench/contrib/markers/browser/markersViewActions.ts index fdd6529ef233..efbf85fa4cfd 100644 --- a/src/vs/workbench/contrib/markers/browser/markersViewActions.ts +++ b/src/vs/workbench/contrib/markers/browser/markersViewActions.ts @@ -9,7 +9,7 @@ import { IContextMenuService } from '../../../../platform/contextview/browser/co import Messages from './messages.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { Marker } from './markersModel.js'; -import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { Event, Emitter } from '../../../../base/common/event.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; @@ -39,20 +39,30 @@ export class MarkersFilters extends Disposable { private readonly _onDidChange: Emitter = this._register(new Emitter()); readonly onDidChange: Event = this._onDidChange.event; - constructor(options: IMarkersFiltersOptions, private readonly contextKeyService: IContextKeyService) { + constructor(options: IMarkersFiltersOptions, contextKeyService: IContextKeyService) { super(); - this._showErrors.set(options.showErrors); - this._showWarnings.set(options.showWarnings); - this._showInfos.set(options.showInfos); + this._excludedFiles = MarkersContextKeys.ShowExcludedFilesFilterContextKey.bindTo(contextKeyService); this._excludedFiles.set(options.excludedFiles); + + this._activeFile = MarkersContextKeys.ShowActiveFileFilterContextKey.bindTo(contextKeyService); this._activeFile.set(options.activeFile); + + this._showWarnings = MarkersContextKeys.ShowWarningsFilterContextKey.bindTo(contextKeyService); + this._showWarnings.set(options.showWarnings); + + this._showInfos = MarkersContextKeys.ShowInfoFilterContextKey.bindTo(contextKeyService); + this._showInfos.set(options.showInfos); + + this._showErrors = MarkersContextKeys.ShowErrorsFilterContextKey.bindTo(contextKeyService); + this._showErrors.set(options.showErrors); + this.filterHistory = options.filterHistory; } filterHistory: string[]; - private readonly _excludedFiles = MarkersContextKeys.ShowExcludedFilesFilterContextKey.bindTo(this.contextKeyService); + private readonly _excludedFiles: IContextKey; get excludedFiles(): boolean { return !!this._excludedFiles.get(); } @@ -63,7 +73,7 @@ export class MarkersFilters extends Disposable { } } - private readonly _activeFile = MarkersContextKeys.ShowActiveFileFilterContextKey.bindTo(this.contextKeyService); + private readonly _activeFile: IContextKey; get activeFile(): boolean { return !!this._activeFile.get(); } @@ -74,7 +84,7 @@ export class MarkersFilters extends Disposable { } } - private readonly _showWarnings = MarkersContextKeys.ShowWarningsFilterContextKey.bindTo(this.contextKeyService); + private readonly _showWarnings: IContextKey; get showWarnings(): boolean { return !!this._showWarnings.get(); } @@ -85,7 +95,7 @@ export class MarkersFilters extends Disposable { } } - private readonly _showErrors = MarkersContextKeys.ShowErrorsFilterContextKey.bindTo(this.contextKeyService); + private readonly _showErrors: IContextKey; get showErrors(): boolean { return !!this._showErrors.get(); } @@ -96,7 +106,7 @@ export class MarkersFilters extends Disposable { } } - private readonly _showInfos = MarkersContextKeys.ShowInfoFilterContextKey.bindTo(this.contextKeyService); + private readonly _showInfos: IContextKey; get showInfos(): boolean { return !!this._showInfos.get(); } diff --git a/src/vs/workbench/contrib/mcp/browser/mcpCommandsAddConfiguration.ts b/src/vs/workbench/contrib/mcp/browser/mcpCommandsAddConfiguration.ts index f61417a662d9..a48cbdd8fafc 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpCommandsAddConfiguration.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpCommandsAddConfiguration.ts @@ -28,6 +28,7 @@ import { IEditorService } from '../../../services/editor/common/editorService.js import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; import { IMcpConfigurationStdio, mcpConfigurationSection, mcpStdioServerSchema } from '../common/mcpConfiguration.js'; import { IMcpRegistry } from '../common/mcpRegistryTypes.js'; +import { IMcpService, McpConnectionState } from '../common/mcpTypes.js'; import { McpServerOptionsCommand } from './mcpCommands.js'; const enum AddConfigurationType { @@ -110,6 +111,7 @@ export class McpAddConfigurationCommand { @IFileService private readonly _fileService: IFileService, @INotificationService private readonly _notificationService: INotificationService, @ITelemetryService private readonly _telemetryService: ITelemetryService, + @IMcpService private readonly _mcpService: IMcpService, ) { } private async getServerType(): Promise { @@ -323,9 +325,11 @@ export class McpAddConfigurationCommand { const store = new DisposableStore(); store.add(autorun(reader => { const colls = this._mcpRegistry.collections.read(reader); + const servers = this._mcpService.servers.read(reader); const match = mapFindFirst(colls, collection => mapFindFirst(collection.serverDefinitions.read(reader), server => server.label === name ? { server, collection } : undefined)); - if (match) { + const server = match && servers.find(s => s.definition.id === match.server.id); + if (match && server) { if (match.collection.presentation?.origin) { this._openerService.openEditor({ resource: match.collection.presentation.origin, @@ -338,6 +342,12 @@ export class McpAddConfigurationCommand { this._commandService.executeCommand(McpServerOptionsCommand.id, name); } + server.start(true).then(state => { + if (state.state === McpConnectionState.Kind.Error) { + server.showOutput(); + } + }); + store.dispose(); } })); diff --git a/src/vs/workbench/contrib/mcp/common/mcpRegistry.ts b/src/vs/workbench/contrib/mcp/common/mcpRegistry.ts index 3a04966d2e75..42990525befd 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpRegistry.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpRegistry.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Codicon } from '../../../../base/common/codicons.js'; import { Emitter } from '../../../../base/common/event.js'; import { StringSHA1 } from '../../../../base/common/hash.js'; import { MarkdownString } from '../../../../base/common/htmlContent.js'; @@ -10,18 +11,21 @@ import { Lazy } from '../../../../base/common/lazy.js'; import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js'; import { derived, IObservable, observableValue } from '../../../../base/common/observable.js'; import { basename } from '../../../../base/common/resources.js'; +import { indexOfPattern } from '../../../../base/common/strings.js'; import { localize } from '../../../../nls.js'; -import { ConfigurationTarget } from '../../../../platform/configuration/common/configuration.js'; +import { ConfigurationTarget, IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; import { observableMemento } from '../../../../platform/observable/common/observableMemento.js'; +import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { IWorkspaceFolderData } from '../../../../platform/workspace/common/workspace.js'; import { IConfigurationResolverService } from '../../../services/configurationResolver/common/configurationResolver.js'; import { ConfigurationResolverExpression, IResolvedValue } from '../../../services/configurationResolver/common/configurationResolverExpression.js'; -import { IEditorService } from '../../../services/editor/common/editorService.js'; +import { AUX_WINDOW_GROUP, IEditorService } from '../../../services/editor/common/editorService.js'; +import { mcpEnabledSection } from './mcpConfiguration.js'; import { McpRegistryInputStorage } from './mcpRegistryInputStorage.js'; import { IMcpHostDelegate, IMcpRegistry, IMcpResolveConnectionOptions } from './mcpRegistryTypes.js'; import { McpServerConnection } from './mcpServerConnection.js'; @@ -41,7 +45,13 @@ export class McpRegistry extends Disposable implements IMcpRegistry { private readonly _collections = observableValue('collections', []); private readonly _delegates: IMcpHostDelegate[] = []; - public readonly collections: IObservable = this._collections; + private readonly _enabled: IObservable; + public readonly collections: IObservable = derived(reader => { + if (!this._enabled.read(reader)) { + return []; + } + return this._collections.read(reader); + }); private readonly _collectionToPrefixes = this._collections.map(c => { // This creates tool prefixes based on a hash of the collection ID. This is @@ -54,7 +64,9 @@ export class McpRegistry extends Disposable implements IMcpRegistry { const hashes = c.map((collection): CollectionHash => { const sha = new StringSHA1(); sha.update(collection.id); - return { view: 0, hash: sha.digest(), collection }; + const hash = sha.digest(); + // Gemini errors if the name starts with a number (microsoft/vscode-copilot-release#7152) + return { view: indexOfPattern(hash, /[a-z]/i), hash, collection }; }); const view = (h: CollectionHash) => h.hash.slice(h.view, h.view + collectionPrefixLen); @@ -84,6 +96,10 @@ export class McpRegistry extends Disposable implements IMcpRegistry { private readonly _ongoingLazyActivations = observableValue(this, 0); public readonly lazyCollectionState = derived(reader => { + if (this._enabled.read(reader) === false) { + return LazyCollectionState.AllKnown; + } + if (this._ongoingLazyActivations.read(reader) > 0) { return LazyCollectionState.LoadingUnknown; } @@ -106,12 +122,16 @@ export class McpRegistry extends Disposable implements IMcpRegistry { @IProductService private readonly _productService: IProductService, @INotificationService private readonly _notificationService: INotificationService, @IEditorService private readonly _editorService: IEditorService, + @IConfigurationService configurationService: IConfigurationService, ) { super(); + this._enabled = observableConfigValue(mcpEnabledSection, true, configurationService); } public registerDelegate(delegate: IMcpHostDelegate): IDisposable { this._delegates.push(delegate); + this._delegates.sort((a, b) => b.priority - a.priority); + return { dispose: () => { const index = this._delegates.indexOf(delegate); @@ -244,9 +264,13 @@ export class McpRegistry extends Disposable implements IMcpRegistry { { message: localize('trustTitleWithOrigin', 'Trust MCP servers from {0}?', collection.label), custom: { + icon: Codicon.shield, markdownDetails: [{ markdown: new MarkdownString(localize('mcp.trust.details', '{0} discovered Model Context Protocol servers from {1} (`{2}`). {0} can use their capabilities in Chat.\n\nDo you want to allow running MCP servers from {3}?', this._productService.nameShort, collection.label, collection.serverDefinitions.get().map(s => s.label).join('`, `'), labelWithOrigin)), - dismissOnLinkClick: true, + actionHandler: () => { + const editor = this._editorService.openEditor({ resource: collection.presentation!.origin! }, AUX_WINDOW_GROUP); + return editor.then(Boolean); + }, }] }, buttons: [ diff --git a/src/vs/workbench/contrib/mcp/common/mcpRegistryInputStorage.ts b/src/vs/workbench/contrib/mcp/common/mcpRegistryInputStorage.ts index 852c34a2a74d..91f473aafda6 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpRegistryInputStorage.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpRegistryInputStorage.ts @@ -139,7 +139,7 @@ export class McpRegistryInputStorage extends Disposable { const encrypted = await crypto.subtle.encrypt( { name: MCP_ENCRYPTION_KEY_ALGORITHM, iv: iv.buffer }, key, - new TextEncoder().encode(toSeal).buffer, + new TextEncoder().encode(toSeal).buffer as ArrayBuffer, ); const enc = encodeBase64(VSBuffer.wrap(new Uint8Array(encrypted))); diff --git a/src/vs/workbench/contrib/mcp/common/mcpRegistryTypes.ts b/src/vs/workbench/contrib/mcp/common/mcpRegistryTypes.ts index e331129d6823..07de8bc1a5be 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpRegistryTypes.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpRegistryTypes.ts @@ -27,6 +27,8 @@ export interface IMcpMessageTransport extends IDisposable { } export interface IMcpHostDelegate { + /** Priority for this delegate, delegates are tested in descending priority order */ + readonly priority: number; waitForInitialProviderPromises(): Promise; canStart(collectionDefinition: McpCollectionDefinition, serverDefinition: McpServerDefinition): boolean; start(collectionDefinition: McpCollectionDefinition, serverDefinition: McpServerDefinition, resolvedLaunch: McpServerLaunch): IMcpMessageTransport; diff --git a/src/vs/workbench/contrib/mcp/common/mcpServer.ts b/src/vs/workbench/contrib/mcp/common/mcpServer.ts index a255998d5b52..e565f38cab3d 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpServer.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpServer.ts @@ -56,7 +56,7 @@ type ServerBootStateClassification = { interface IToolCacheEntry { /** Cached tools so we can show what's available before it's started */ - readonly tools: readonly MCP.Tool[]; + readonly tools: readonly IValidatedMcpTool[]; } interface IServerCacheEntry { @@ -109,12 +109,12 @@ export class McpServerMetadataCache extends Disposable { } /** Gets cached tools for a server (used before a server is running) */ - getTools(definitionId: string): readonly MCP.Tool[] | undefined { + getTools(definitionId: string): readonly IValidatedMcpTool[] | undefined { return this.cache.get(definitionId)?.tools; } /** Sets cached tools for a server */ - storeTools(definitionId: string, tools: readonly MCP.Tool[]): void { + storeTools(definitionId: string, tools: readonly IValidatedMcpTool[]): void { this.cache.set(definitionId, { ...this.cache.get(definitionId), tools }); this.didChange = true; } @@ -135,6 +135,15 @@ export class McpServerMetadataCache extends Disposable { } } +interface IValidatedMcpTool extends MCP.Tool { + /** + * Tool name as published by the MCP server. This may + * be different than the one in {@link definition} due to name normalization + * in {@link McpServer._getValidatedTools}. + */ + serverToolName: string; +} + export class McpServer extends Disposable implements IMcpServer { private readonly _connectionSequencer = new Sequencer(); private readonly _connection = this._register(disposableObservableValue(this, undefined)); @@ -145,7 +154,7 @@ export class McpServer extends Disposable implements IMcpServer { private get toolsFromCache() { return this._toolCache.getTools(this.definition.id); } - private readonly toolsFromServerPromise = observableValue | undefined>(this, undefined); + private readonly toolsFromServerPromise = observableValue | undefined>(this, undefined); private readonly toolsFromServer = derived(reader => this.toolsFromServerPromise.read(reader)?.promiseResult.read(reader)?.data); public readonly tools: IObservable; @@ -311,7 +320,8 @@ export class McpServer extends Disposable implements IMcpServer { }); } - private async _normalizeTool(tool: MCP.Tool): Promise { + private async _normalizeTool(originalTool: MCP.Tool): Promise { + const tool: IValidatedMcpTool = { ...originalTool, serverToolName: originalTool.name }; if (!tool.description) { // Ensure a description is provided for each tool, #243919 this._logger.warn(`Tool ${tool.name} does not have a description. Tools must be accurately described to be called`); @@ -349,11 +359,11 @@ export class McpServer extends Disposable implements IMcpServer { return { error: messages }; } - private async _getValidatedTools(handler: McpServerRequestHandler, tools: MCP.Tool[]) { + private async _getValidatedTools(handler: McpServerRequestHandler, tools: MCP.Tool[]): Promise { let error = ''; const validations = await Promise.all(tools.map(t => this._normalizeTool(t))); - const validated: MCP.Tool[] = []; + const validated: IValidatedMcpTool[] = []; for (const [i, result] of validations.entries()) { if ('error' in result) { error += localize('mcpBadSchema.tool', 'Tool `{0}` has invalid JSON parameters:', tools[i].name) + '\n'; @@ -458,16 +468,20 @@ export class McpTool implements IMcpTool { readonly id: string; + public get definition(): MCP.Tool { return this._definition; } + constructor( private readonly _server: McpServer, idPrefix: string, - public readonly definition: MCP.Tool, + private readonly _definition: IValidatedMcpTool, ) { - this.id = (idPrefix + definition.name).replaceAll('.', '_'); + this.id = (idPrefix + _definition.name).replaceAll('.', '_'); } call(params: Record, token?: CancellationToken): Promise { - return this._server.callOn(h => h.callTool({ name: this.definition.name, arguments: params }), token); + // serverToolName is always set now, but older cache entries (from 1.99-Insiders) may not have it. + const name = this._definition.serverToolName ?? this._definition.name; + return this._server.callOn(h => h.callTool({ name, arguments: params }), token); } } diff --git a/src/vs/workbench/contrib/mcp/common/mcpServerConnection.ts b/src/vs/workbench/contrib/mcp/common/mcpServerConnection.ts index f367162fd6ba..30fff6f2f16d 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpServerConnection.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpServerConnection.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationTokenSource } from '../../../../base/common/cancellation.js'; +import { CancellationError } from '../../../../base/common/errors.js'; import { Disposable, DisposableStore, IReference, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { autorun, IObservable, observableValue } from '../../../../base/common/observable.js'; import { localize } from '../../../../nls.js'; @@ -84,11 +85,17 @@ export class McpServerConnection extends Disposable implements IMcpServerConnect } }, err => { - store.dispose(); if (!store.isDisposed) { - this._logger.error(err); - this._state.set({ state: McpConnectionState.Kind.Error, message: `Could not initialize MCP server: ${err.message}` }, undefined); + let message = err.message; + if (err instanceof CancellationError) { + message = 'Server exited before responding to `initialize` request.'; + this._logger.error(message); + } else { + this._logger.error(err); + } + this._state.set({ state: McpConnectionState.Kind.Error, message }, undefined); } + store.dispose(); }, ); } diff --git a/src/vs/workbench/contrib/mcp/test/common/mcpRegistry.test.ts b/src/vs/workbench/contrib/mcp/test/common/mcpRegistry.test.ts index bcbbc95a0747..04baff6b84df 100644 --- a/src/vs/workbench/contrib/mcp/test/common/mcpRegistry.test.ts +++ b/src/vs/workbench/contrib/mcp/test/common/mcpRegistry.test.ts @@ -10,7 +10,7 @@ import { ISettableObservable, observableValue } from '../../../../../base/common import { upcast } from '../../../../../base/common/types.js'; import { URI } from '../../../../../base/common/uri.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; -import { ConfigurationTarget } from '../../../../../platform/configuration/common/configuration.js'; +import { ConfigurationTarget, IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js'; import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; @@ -28,6 +28,8 @@ import { McpServerConnection } from '../../common/mcpServerConnection.js'; import { LazyCollectionState, McpCollectionDefinition, McpCollectionReference, McpServerDefinition, McpServerTransportType } from '../../common/mcpTypes.js'; import { TestMcpMessageTransport } from './mcpRegistryTypes.js'; import { ConfigurationResolverExpression } from '../../../../services/configurationResolver/common/configurationResolverExpression.js'; +import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; +import { mcpEnabledSection } from '../../common/mcpConfiguration.js'; class TestConfigurationResolverService implements Partial { declare readonly _serviceBrand: undefined; @@ -72,6 +74,8 @@ class TestConfigurationResolverService implements Partial { let testDialogService: TestDialogService; let testCollection: McpCollectionDefinition & { serverDefinitions: ISettableObservable }; let baseDefinition: McpServerDefinition; + let configurationService: TestConfigurationService; let logger: ILogger; setup(() => { testConfigResolverService = new TestConfigurationResolverService(); testStorageService = store.add(new TestStorageService()); testDialogService = new TestDialogService(); + configurationService = new TestConfigurationService({ [mcpEnabledSection]: true }); const services = new ServiceCollection( + [IConfigurationService, configurationService], [IConfigurationResolverService, testConfigResolverService], [IStorageService, testStorageService], [ISecretStorageService, new TestSecretStorageService()], @@ -178,6 +185,21 @@ suite('Workbench - MCP - Registry', () => { assert.strictEqual(registry.collections.get().length, 0); }); + test('collections are not visible when not enabled', () => { + const disposable = registry.registerCollection(testCollection); + store.add(disposable); + + assert.strictEqual(registry.collections.get().length, 1); + + configurationService.setUserConfiguration(mcpEnabledSection, false); + configurationService.onDidChangeConfigurationEmitter.fire({ affectsConfiguration: () => true } as any); + + assert.strictEqual(registry.collections.get().length, 0); + + configurationService.setUserConfiguration(mcpEnabledSection, true); + configurationService.onDidChangeConfigurationEmitter.fire({ affectsConfiguration: () => true } as any); + }); + test('registerDelegate adds delegate to registry', () => { const delegate = new TestMcpHostDelegate(); const disposable = registry.registerDelegate(delegate); @@ -551,6 +573,23 @@ suite('Workbench - MCP - Registry', () => { assert.strictEqual(prefix2, ''); }); + test('prefix does not start with a number', () => { + const collection: McpCollectionDefinition = { + id: 'foo', + label: 'Collection 1', + remoteAuthority: null, + serverDefinitions: observableValue('serverDefs', []), + isTrustedByDefault: true, + scope: StorageScope.APPLICATION + }; + + const disposable = registry.registerCollection(collection); + store.add(disposable); + + const prefix1 = registry.collectionToolPrefix(collection).get(); + assert.strictEqual(prefix1, 'bee.'); // normally 0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33 + }); + test('prefix is empty for unknown collections', () => { const unknownCollection: McpCollectionReference = { id: 'unknown', diff --git a/src/vs/workbench/contrib/mcp/test/common/mcpServerConnection.test.ts b/src/vs/workbench/contrib/mcp/test/common/mcpServerConnection.test.ts index 048a8f7d1ced..bcc62930a842 100644 --- a/src/vs/workbench/contrib/mcp/test/common/mcpServerConnection.test.ts +++ b/src/vs/workbench/contrib/mcp/test/common/mcpServerConnection.test.ts @@ -26,6 +26,8 @@ class TestMcpHostDelegate extends Disposable implements IMcpHostDelegate { private readonly _transport: TestMcpMessageTransport; private _canStartValue = true; + priority = 0; + constructor() { super(); this._transport = this._register(new TestMcpMessageTransport()); diff --git a/src/vs/workbench/contrib/mcp/test/common/mcpServerRequestHandler.test.ts b/src/vs/workbench/contrib/mcp/test/common/mcpServerRequestHandler.test.ts index ebbfeb757981..205465a0f08a 100644 --- a/src/vs/workbench/contrib/mcp/test/common/mcpServerRequestHandler.test.ts +++ b/src/vs/workbench/contrib/mcp/test/common/mcpServerRequestHandler.test.ts @@ -24,6 +24,8 @@ import { CancellationTokenSource } from '../../../../../base/common/cancellation class TestMcpHostDelegate extends Disposable implements IMcpHostDelegate { private readonly _transport: TestMcpMessageTransport; + priority = 0; + constructor() { super(); this._transport = this._register(new TestMcpMessageTransport()); diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts index 666ad3f373ee..57587b04cab2 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts @@ -226,12 +226,19 @@ export class MergeEditor extends AbstractTextEditor { this._ctxResultUri.reset(); })); + const viewZoneRegistrationStore = new DisposableStore(); + this._sessionDisposables.add(viewZoneRegistrationStore); // Set the view zones before restoring view state! // Otherwise scrolling will be off - this._sessionDisposables.add(autorunWithStore((reader, store) => { + this._sessionDisposables.add(autorunWithStore((reader) => { /** @description update alignment view zones */ const baseView = this.baseView.read(reader); + const resultScrollTop = this.inputResultView.editor.getScrollTop(); + this.scrollSynchronizer.stopSync(); + + viewZoneRegistrationStore.clear(); + this.inputResultView.editor.changeViewZones(resultViewZoneAccessor => { const layout = this._layoutModeObs.read(reader); const shouldAlignResult = layout.kind === 'columns'; @@ -241,7 +248,7 @@ export class MergeEditor extends AbstractTextEditor { this.input2View.editor.changeViewZones(input2ViewZoneAccessor => { if (baseView) { baseView.editor.changeViewZones(baseViewZoneAccessor => { - store.add(this.setViewZones(reader, + viewZoneRegistrationStore.add(this.setViewZones(reader, viewModel, this.input1View.editor, input1ViewZoneAccessor, @@ -256,7 +263,7 @@ export class MergeEditor extends AbstractTextEditor { )); }); } else { - store.add(this.setViewZones(reader, + viewZoneRegistrationStore.add(this.setViewZones(reader, viewModel, this.input1View.editor, input1ViewZoneAccessor, @@ -274,6 +281,9 @@ export class MergeEditor extends AbstractTextEditor { }); }); + this.inputResultView.editor.setScrollTop(resultScrollTop, ScrollType.Smooth); + + this.scrollSynchronizer.startSync(); this.scrollSynchronizer.updateScrolling(); })); diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/scrollSynchronizer.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/scrollSynchronizer.ts index 4d09d327f293..dec1af45d1bb 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 { autorunWithStore, IObservable } from '../../../../../base/common/observable.js'; +import { derivedWithStore, 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'; @@ -14,6 +14,9 @@ import { IMergeEditorLayout } from './mergeEditor.js'; import { MergeEditorViewModel } from './viewModel.js'; import { InputCodeEditorView } from './editors/inputCodeEditorView.js'; import { ResultCodeEditorView } from './editors/resultCodeEditorView.js'; +import { CodeEditorView } from './editors/codeEditorView.js'; +import { BugIndicatingError } from '../../../../../base/common/errors.js'; +import { isDefined } from '../../../../../base/common/types.js'; export class ScrollSynchronizer extends Disposable { private get model() { return this.viewModel.get()?.model; } @@ -22,8 +25,10 @@ export class ScrollSynchronizer extends Disposable { public readonly updateScrolling: () => void; - private get shouldAlignResult() { return this.layout.get().kind === 'columns'; } - private get shouldAlignBase() { return this.layout.get().kind === 'mixed' && !this.layout.get().showBaseAtTop; } + private get lockResultWithInputs() { return this.layout.get().kind === 'columns'; } + private get lockBaseWithInputs() { return this.layout.get().kind === 'mixed' && !this.layout.get().showBaseAtTop; } + + private _isSyncing = true; constructor( private readonly viewModel: IObservable, @@ -35,149 +40,132 @@ export class ScrollSynchronizer extends Disposable { ) { super(); - const handleInput1OnScroll = this.updateScrolling = () => { - if (!this.model) { - return; - } + const s = derivedWithStore((reader, store) => { + const baseView = this.baseView.read(reader); + const editors = [this.input1View, this.input2View, this.inputResultView, baseView].filter(isDefined); - this.input2View.editor.setScrollTop(this.input1View.editor.getScrollTop(), ScrollType.Immediate); - - if (this.shouldAlignResult) { - this.inputResultView.editor.setScrollTop(this.input1View.editor.getScrollTop(), ScrollType.Immediate); - } else { - const mappingInput1Result = this.model.input1ResultMapping.get(); - this.synchronizeScrolling(this.input1View.editor, this.inputResultView.editor, mappingInput1Result); - } - - const baseView = this.baseView.get(); - if (baseView) { - if (this.shouldAlignBase) { - this.baseView.get()?.editor.setScrollTop(this.input1View.editor.getScrollTop(), ScrollType.Immediate); - } else { - const mapping = new DocumentLineRangeMap(this.model.baseInput1Diffs.get(), -1).reverse(); - this.synchronizeScrolling(this.input1View.editor, baseView.editor, mapping); - } - } - }; - - this._store.add( - this.input1View.editor.onDidScrollChange( - this.reentrancyBarrier.makeExclusiveOrSkip((c) => { - if (c.scrollTopChanged) { - handleInput1OnScroll(); + const alignScrolling = (source: CodeEditorView, updateScrollLeft: boolean, updateScrollTop: boolean) => { + this.reentrancyBarrier.runExclusivelyOrSkip(() => { + if (updateScrollLeft) { + const scrollLeft = source.editor.getScrollLeft(); + for (const editorView of editors) { + if (editorView !== source) { + editorView.editor.setScrollLeft(scrollLeft, ScrollType.Immediate); + } + } } - if (c.scrollLeftChanged) { - this.baseView.get()?.editor.setScrollLeft(c.scrollLeft, ScrollType.Immediate); - this.input2View.editor.setScrollLeft(c.scrollLeft, ScrollType.Immediate); - this.inputResultView.editor.setScrollLeft(c.scrollLeft, ScrollType.Immediate); + if (updateScrollTop) { + const scrollTop = source.editor.getScrollTop(); + for (const editorView of editors) { + if (editorView !== source) { + if (this._shouldLock(source, editorView)) { + editorView.editor.setScrollTop(scrollTop, ScrollType.Immediate); + } else { + const m = this._getMapping(source, editorView); + if (m) { + this._synchronizeScrolling(source.editor, editorView.editor, m); + } + } + } + } } - }) - ) - ); + }); + }; - this._store.add( - this.input2View.editor.onDidScrollChange( - this.reentrancyBarrier.makeExclusiveOrSkip((c) => { - if (!this.model) { + for (const editorView of editors) { + store.add(editorView.editor.onDidScrollChange(e => { + if (!this._isSyncing) { return; } + alignScrolling(editorView, e.scrollLeftChanged, e.scrollTopChanged); + })); + } - if (c.scrollTopChanged) { - this.input1View.editor.setScrollTop(c.scrollTop, ScrollType.Immediate); - - if (this.shouldAlignResult) { - this.inputResultView.editor.setScrollTop(this.input2View.editor.getScrollTop(), ScrollType.Immediate); - } else { - const mappingInput2Result = this.model.input2ResultMapping.get(); - this.synchronizeScrolling(this.input2View.editor, this.inputResultView.editor, mappingInput2Result); - } - - const baseView = this.baseView.get(); - if (baseView && this.model) { - if (this.shouldAlignBase) { - this.baseView.get()?.editor.setScrollTop(c.scrollTop, ScrollType.Immediate); - } else { - const mapping = new DocumentLineRangeMap(this.model.baseInput2Diffs.get(), -1).reverse(); - this.synchronizeScrolling(this.input2View.editor, baseView.editor, mapping); - } - } - } - if (c.scrollLeftChanged) { - this.baseView.get()?.editor.setScrollLeft(c.scrollLeft, ScrollType.Immediate); - this.input1View.editor.setScrollLeft(c.scrollLeft, ScrollType.Immediate); - this.inputResultView.editor.setScrollLeft(c.scrollLeft, ScrollType.Immediate); - } - }) - ) - ); - this._store.add( - this.inputResultView.editor.onDidScrollChange( - this.reentrancyBarrier.makeExclusiveOrSkip((c) => { - if (c.scrollTopChanged) { - if (this.shouldAlignResult) { - this.input1View.editor.setScrollTop(c.scrollTop, ScrollType.Immediate); - this.input2View.editor.setScrollTop(c.scrollTop, ScrollType.Immediate); - } else { - const mapping1 = this.model?.resultInput1Mapping.get(); - this.synchronizeScrolling(this.inputResultView.editor, this.input1View.editor, mapping1); - - const mapping2 = this.model?.resultInput2Mapping.get(); - this.synchronizeScrolling(this.inputResultView.editor, this.input2View.editor, mapping2); - } - - const baseMapping = this.model?.resultBaseMapping.get(); - const baseView = this.baseView.get(); - if (baseView && this.model) { - this.synchronizeScrolling(this.inputResultView.editor, baseView.editor, baseMapping); - } - } - if (c.scrollLeftChanged) { - this.baseView.get()?.editor?.setScrollLeft(c.scrollLeft, ScrollType.Immediate); - this.input1View.editor.setScrollLeft(c.scrollLeft, ScrollType.Immediate); - this.input2View.editor.setScrollLeft(c.scrollLeft, ScrollType.Immediate); - } - }) - ) - ); - - this._store.add( - autorunWithStore((reader, store) => { - /** @description set baseViewEditor.onDidScrollChange */ - const baseView = this.baseView.read(reader); - if (baseView) { - store.add(baseView.editor.onDidScrollChange( - this.reentrancyBarrier.makeExclusiveOrSkip((c) => { - if (c.scrollTopChanged) { - if (!this.model) { - return; - } - if (this.shouldAlignBase) { - this.input1View.editor.setScrollTop(c.scrollTop, ScrollType.Immediate); - this.input2View.editor.setScrollTop(c.scrollTop, ScrollType.Immediate); - } else { - const baseInput1Mapping = new DocumentLineRangeMap(this.model.baseInput1Diffs.get(), -1); - this.synchronizeScrolling(baseView.editor, this.input1View.editor, baseInput1Mapping); - - const baseInput2Mapping = new DocumentLineRangeMap(this.model.baseInput2Diffs.get(), -1); - this.synchronizeScrolling(baseView.editor, this.input2View.editor, baseInput2Mapping); - } - - const baseMapping = this.model?.baseResultMapping.get(); - this.synchronizeScrolling(baseView.editor, this.inputResultView.editor, baseMapping); - } - if (c.scrollLeftChanged) { - this.inputResultView.editor.setScrollLeft(c.scrollLeft, ScrollType.Immediate); - this.input1View.editor.setScrollLeft(c.scrollLeft, ScrollType.Immediate); - this.input2View.editor.setScrollLeft(c.scrollLeft, ScrollType.Immediate); - } - }) - )); + return { + update: () => { + alignScrolling(this.inputResultView, true, true); } - }) - ); + }; + }).recomputeInitiallyAndOnChange(this._store); + + this.updateScrolling = () => { + s.get().update(); + }; } - private synchronizeScrolling(scrollingEditor: CodeEditorWidget, targetEditor: CodeEditorWidget, mapping: DocumentLineRangeMap | undefined) { + public stopSync(): void { + this._isSyncing = false; + } + + public startSync(): void { + this._isSyncing = true; + } + + private _shouldLock(editor1: CodeEditorView, editor2: CodeEditorView): boolean { + const isInput = (editor: CodeEditorView) => editor === this.input1View || editor === this.input2View; + if (isInput(editor1) && editor2 === this.inputResultView || isInput(editor2) && editor1 === this.inputResultView) { + return this.lockResultWithInputs; + } + if (isInput(editor1) && editor2 === this.baseView.get() || isInput(editor2) && editor1 === this.baseView.get()) { + return this.lockBaseWithInputs; + } + if (isInput(editor1) && isInput(editor2)) { + return true; + } + return false; + } + + private _getMapping(editor1: CodeEditorView, editor2: CodeEditorView): DocumentLineRangeMap | undefined { + if (editor1 === this.input1View) { + if (editor2 === this.input2View) { + return undefined; + } else if (editor2 === this.inputResultView) { + return this.model?.input1ResultMapping.get()!; + } else if (editor2 === this.baseView.get()) { + const b = this.model?.baseInput1Diffs.get(); + if (!b) { return undefined; } + return new DocumentLineRangeMap(b, -1).reverse(); + } + } else if (editor1 === this.input2View) { + if (editor2 === this.input1View) { + return undefined; + } else if (editor2 === this.inputResultView) { + return this.model?.input2ResultMapping.get()!; + } else if (editor2 === this.baseView.get()) { + const b = this.model?.baseInput2Diffs.get(); + if (!b) { return undefined; } + return new DocumentLineRangeMap(b, -1).reverse(); + } + } else if (editor1 === this.inputResultView) { + if (editor2 === this.input1View) { + return this.model?.resultInput1Mapping.get()!; + } else if (editor2 === this.input2View) { + return this.model?.resultInput2Mapping.get()!; + } else if (editor2 === this.baseView.get()) { + const b = this.model?.resultBaseMapping.get(); + if (!b) { return undefined; } + return b; + } + } else if (editor1 === this.baseView.get()) { + if (editor2 === this.input1View) { + const b = this.model?.baseInput1Diffs.get(); + if (!b) { return undefined; } + return new DocumentLineRangeMap(b, -1); + } else if (editor2 === this.input2View) { + const b = this.model?.baseInput2Diffs.get(); + if (!b) { return undefined; } + return new DocumentLineRangeMap(b, -1); + } else if (editor2 === this.inputResultView) { + const b = this.model?.baseResultMapping.get(); + if (!b) { return undefined; } + return b; + } + } + + throw new BugIndicatingError(); + } + + private _synchronizeScrolling(scrollingEditor: CodeEditorWidget, targetEditor: CodeEditorWidget, mapping: DocumentLineRangeMap | undefined) { if (!mapping) { return; } 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 1875c7304c36..58a076cc8cdc 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 @@ -19,7 +19,7 @@ import { ServicesAccessor } from '../../../../../../platform/instantiation/commo import { IQuickInputService, IQuickPickItem } 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 } from '../../../../chat/browser/chat.js'; +import { IChatWidget, IChatWidgetService, showChatView } from '../../../../chat/browser/chat.js'; import { ChatInputPart } from '../../../../chat/browser/chatInputPart.js'; import { ChatDynamicVariableModel } from '../../../../chat/browser/contrib/chatDynamicVariables.js'; import { computeCompletionRanges } from '../../../../chat/browser/contrib/chatInputCompletions.js'; @@ -37,6 +37,7 @@ import { INotebookOutputActionContext, NOTEBOOK_ACTIONS_CATEGORY } from '../core import { CellUri } from '../../../common/notebookCommon.js'; import './cellChatActions.js'; import { CTX_NOTEBOOK_CHAT_HAS_AGENT } from './notebookChatContext.js'; +import { IViewsService } from '../../../../../services/views/common/viewsService.js'; const NotebookKernelVariableKey = 'kernelVariable'; const NOTEBOOK_CELL_OUTPUT_MIME_TYPE_LIST_FOR_CHAT_CONST = ['text/plain', 'text/html', @@ -276,6 +277,7 @@ registerAction2(class CopyCellOutputAction extends Action2 { async run(accessor: ServicesAccessor, outputContext: INotebookOutputActionContext | { outputViewModel: ICellOutputViewModel } | undefined): Promise { const notebookEditor = this.getNoteboookEditor(accessor.get(IEditorService), outputContext); + const viewService = accessor.get(IViewsService); if (!notebookEditor) { return; @@ -364,6 +366,7 @@ registerAction2(class CopyCellOutputAction extends Action2 { isFile: true, }; widget.attachmentModel.addContext(l); + (await showChatView(viewService))?.focusInput(); } } diff --git a/src/vs/workbench/contrib/output/browser/outputServices.ts b/src/vs/workbench/contrib/output/browser/outputServices.ts index f644a0f2cc12..bd0506795c62 100644 --- a/src/vs/workbench/contrib/output/browser/outputServices.ts +++ b/src/vs/workbench/contrib/output/browser/outputServices.ts @@ -108,11 +108,22 @@ class OutputViewFilters extends Disposable implements IOutputViewFilters { ) { super(); + this._trace = SHOW_TRACE_FILTER_CONTEXT.bindTo(this.contextKeyService); this._trace.set(options.trace); + + this._debug = SHOW_DEBUG_FILTER_CONTEXT.bindTo(this.contextKeyService); this._debug.set(options.debug); + + this._info = SHOW_INFO_FILTER_CONTEXT.bindTo(this.contextKeyService); this._info.set(options.info); + + this._warning = SHOW_WARNING_FILTER_CONTEXT.bindTo(this.contextKeyService); this._warning.set(options.warning); + + this._error = SHOW_ERROR_FILTER_CONTEXT.bindTo(this.contextKeyService); this._error.set(options.error); + + this._categories = HIDE_CATEGORY_FILTER_CONTEXT.bindTo(this.contextKeyService); this._categories.set(options.sources); this.filterHistory = options.filterHistory; @@ -131,7 +142,7 @@ class OutputViewFilters extends Disposable implements IOutputViewFilters { } } - private readonly _trace = SHOW_TRACE_FILTER_CONTEXT.bindTo(this.contextKeyService); + private readonly _trace: IContextKey; get trace(): boolean { return !!this._trace.get(); } @@ -142,7 +153,7 @@ class OutputViewFilters extends Disposable implements IOutputViewFilters { } } - private readonly _debug = SHOW_DEBUG_FILTER_CONTEXT.bindTo(this.contextKeyService); + private readonly _debug: IContextKey; get debug(): boolean { return !!this._debug.get(); } @@ -153,7 +164,7 @@ class OutputViewFilters extends Disposable implements IOutputViewFilters { } } - private readonly _info = SHOW_INFO_FILTER_CONTEXT.bindTo(this.contextKeyService); + private readonly _info: IContextKey; get info(): boolean { return !!this._info.get(); } @@ -164,7 +175,7 @@ class OutputViewFilters extends Disposable implements IOutputViewFilters { } } - private readonly _warning = SHOW_WARNING_FILTER_CONTEXT.bindTo(this.contextKeyService); + private readonly _warning: IContextKey; get warning(): boolean { return !!this._warning.get(); } @@ -175,7 +186,7 @@ class OutputViewFilters extends Disposable implements IOutputViewFilters { } } - private readonly _error = SHOW_ERROR_FILTER_CONTEXT.bindTo(this.contextKeyService); + private readonly _error: IContextKey; get error(): boolean { return !!this._error.get(); } @@ -186,7 +197,7 @@ class OutputViewFilters extends Disposable implements IOutputViewFilters { } } - private readonly _categories = HIDE_CATEGORY_FILTER_CONTEXT.bindTo(this.contextKeyService); + private readonly _categories: IContextKey; get categories(): string { return this._categories.get() || ','; } diff --git a/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts b/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts index 3ae6694f9646..2de14ff31ee8 100644 --- a/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts +++ b/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts @@ -50,7 +50,7 @@ export class CommandsQuickAccessProvider extends AbstractEditorCommandsQuickAcce // a chance to register so that the complete set of commands shows up as result // We do not want to delay functionality beyond that time though to keep the commands // functional. - private readonly extensionRegistrationRace = raceTimeout(this.extensionService.whenInstalledExtensionsRegistered(), 800); + private readonly extensionRegistrationRace: Promise; private useAiRelatedInfo = false; @@ -67,7 +67,7 @@ export class CommandsQuickAccessProvider extends AbstractEditorCommandsQuickAcce constructor( @IEditorService private readonly editorService: IEditorService, @IMenuService private readonly menuService: IMenuService, - @IExtensionService private readonly extensionService: IExtensionService, + @IExtensionService extensionService: IExtensionService, @IInstantiationService instantiationService: IInstantiationService, @IKeybindingService keybindingService: IKeybindingService, @ICommandService commandService: ICommandService, @@ -88,6 +88,7 @@ export class CommandsQuickAccessProvider extends AbstractEditorCommandsQuickAcce }), }, instantiationService, keybindingService, commandService, telemetryService, dialogService); + this.extensionRegistrationRace = raceTimeout(extensionService.whenInstalledExtensionsRegistered(), 800); this._register(configurationService.onDidChangeConfiguration((e) => this.updateOptions(e))); this.updateOptions(); } diff --git a/src/vs/workbench/contrib/scm/browser/quickDiffModel.ts b/src/vs/workbench/contrib/scm/browser/quickDiffModel.ts index ce0f83a212f5..a138a78aebd2 100644 --- a/src/vs/workbench/contrib/scm/browser/quickDiffModel.ts +++ b/src/vs/workbench/contrib/scm/browser/quickDiffModel.ts @@ -26,7 +26,7 @@ import { LineRangeMapping } from '../../../../editor/common/diff/rangeMapping.js import { IDiffEditorModel } from '../../../../editor/common/editorCommon.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IProgressService, ProgressLocation } from '../../../../platform/progress/common/progress.js'; -import { IChatEditingService, WorkingSetEntryState } from '../../chat/common/chatEditingService.js'; +import { IChatEditingService, ModifiedFileEntryState } from '../../chat/common/chatEditingService.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { autorun, autorunWithStore } from '../../../../base/common/observable.js'; @@ -358,7 +358,7 @@ export class QuickDiffModel extends Disposable { // disable dirty diff when doing chat edits const isBeingModifiedByChatEdits = this._chatEditingService.editingSessionsObs.get() - .some(session => session.getEntry(uri)?.state.get() === WorkingSetEntryState.Modified); + .some(session => session.getEntry(uri)?.state.get() === ModifiedFileEntryState.Modified); if (isBeingModifiedByChatEdits) { return Promise.resolve([]); } diff --git a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts index 2fcb98375e2e..3b338f7cac3f 100644 --- a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts +++ b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts @@ -833,7 +833,6 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { eventCounter++; this._busyTasks[mapKey] = task; this._fireTaskEvent(TaskEvent.general(TaskEventKind.Active, task, terminal?.instanceId)); - this._fireTaskEvent(TaskEvent.general(TaskEventKind.ProblemMatcherStarted, task, terminal?.instanceId)); } else if (event.kind === ProblemCollectorEventKind.BackgroundProcessingEnds) { eventCounter--; if (this._busyTasks[mapKey]) { @@ -882,6 +881,7 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { this._fireTaskEvent(TaskEvent.start(task, terminal.instanceId, resolver.values)); let onData: IDisposable | undefined; if (problemMatchers.length) { + this._fireTaskEvent(TaskEvent.general(TaskEventKind.ProblemMatcherStarted, task, terminal.instanceId)); // prevent https://github.com/microsoft/vscode/issues/174511 from happening onData = terminal.onLineData((line) => { watchingProblemMatcher.processLine(line); diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index be6d87dec84f..851fb136c3cd 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -14,7 +14,7 @@ import { createDecorator } from '../../../../platform/instantiation/common/insta import { IKeyMods } from '../../../../platform/quickinput/common/quickInput.js'; import { IMarkProperties, ITerminalCapabilityImplMap, ITerminalCapabilityStore, ITerminalCommand, TerminalCapability } from '../../../../platform/terminal/common/capabilities/capabilities.js'; import { IMergedEnvironmentVariableCollection } from '../../../../platform/terminal/common/environmentVariable.js'; -import { IExtensionTerminalProfile, IReconnectionProperties, IShellIntegration, IShellLaunchConfig, ITerminalBackend, ITerminalDimensions, ITerminalLaunchError, ITerminalProfile, ITerminalTabLayoutInfoById, TerminalExitReason, TerminalIcon, TerminalLocation, TerminalShellType, TerminalType, TitleEventSource, WaitOnExitValue, type IDecorationAddon } from '../../../../platform/terminal/common/terminal.js'; +import { IExtensionTerminalProfile, IReconnectionProperties, IShellIntegration, IShellLaunchConfig, ITerminalBackend, ITerminalDimensions, ITerminalLaunchError, ITerminalProfile, ITerminalTabLayoutInfoById, TerminalExitReason, TerminalIcon, TerminalLocation, TerminalShellType, TerminalType, TitleEventSource, WaitOnExitValue, type IDecorationAddon, type ShellIntegrationInjectionFailureReason } from '../../../../platform/terminal/common/terminal.js'; import { IColorTheme } from '../../../../platform/theme/common/themeService.js'; import { IWorkspaceFolder } from '../../../../platform/workspace/common/workspace.js'; import { EditorInput } from '../../../common/editor/editorInput.js'; @@ -619,6 +619,7 @@ export interface ITerminalInstance extends IBaseTerminalInstance { readonly initialCwd?: string; readonly os?: OperatingSystem; readonly usedShellIntegrationInjection: boolean; + readonly shellIntegrationInjectionFailureReason: ShellIntegrationInjectionFailureReason | undefined; readonly injectedArgs: string[] | undefined; readonly extEnvironmentVariableCollection: IMergedEnvironmentVariableCollection | undefined; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 8149a20e575e..bb491690bbe9 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -46,7 +46,7 @@ import { IMarkProperties, TerminalCapability } from '../../../../platform/termin import { TerminalCapabilityStoreMultiplexer } from '../../../../platform/terminal/common/capabilities/terminalCapabilityStore.js'; import { IEnvironmentVariableCollection, IMergedEnvironmentVariableCollection } from '../../../../platform/terminal/common/environmentVariable.js'; import { deserializeEnvironmentVariableCollections } from '../../../../platform/terminal/common/environmentVariableShared.js'; -import { GeneralShellType, IProcessDataEvent, IProcessPropertyMap, IReconnectionProperties, IShellLaunchConfig, ITerminalDimensionsOverride, ITerminalLaunchError, ITerminalLogService, PosixShellType, ProcessPropertyType, ShellIntegrationStatus, TerminalExitReason, TerminalIcon, TerminalLocation, TerminalSettingId, TerminalShellType, TitleEventSource, WindowsShellType } from '../../../../platform/terminal/common/terminal.js'; +import { GeneralShellType, IProcessDataEvent, IProcessPropertyMap, IReconnectionProperties, IShellLaunchConfig, ITerminalDimensionsOverride, ITerminalLaunchError, ITerminalLogService, PosixShellType, ProcessPropertyType, ShellIntegrationStatus, TerminalExitReason, TerminalIcon, TerminalLocation, TerminalSettingId, TerminalShellType, TitleEventSource, WindowsShellType, type ShellIntegrationInjectionFailureReason } from '../../../../platform/terminal/common/terminal.js'; import { formatMessageForTerminal } from '../../../../platform/terminal/common/terminalStrings.js'; import { editorBackground } from '../../../../platform/theme/common/colorRegistry.js'; import { getIconRegistry } from '../../../../platform/theme/common/iconRegistry.js'; @@ -193,6 +193,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { private _hasScrollBar?: boolean; private _usedShellIntegrationInjection: boolean = false; get usedShellIntegrationInjection(): boolean { return this._usedShellIntegrationInjection; } + private _shellIntegrationInjectionInfo: ShellIntegrationInjectionFailureReason | undefined; + get shellIntegrationInjectionFailureReason(): ShellIntegrationInjectionFailureReason | undefined { return this._shellIntegrationInjectionInfo; } private _lineDataEventAddon: LineDataEventAddon | undefined; private readonly _scopedContextKeyService: IContextKeyService; private _resizeDebouncer?: TerminalResizeDebouncer; @@ -1457,6 +1459,9 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { case ProcessPropertyType.UsedShellIntegrationInjection: this._usedShellIntegrationInjection = true; break; + case ProcessPropertyType.ShellIntegrationInjectionFailureReason: + this._shellIntegrationInjectionInfo = value; + break; } })); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTelemetry.ts b/src/vs/workbench/contrib/terminal/browser/terminalTelemetry.ts index 6823a176622c..e321b696b4b6 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTelemetry.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTelemetry.ts @@ -3,131 +3,259 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable } from '../../../../base/common/lifecycle.js'; +import { timeout } from '../../../../base/common/async.js'; +import { Event } from '../../../../base/common/event.js'; +import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; import { basename } from '../../../../base/common/path.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; -import type { IShellLaunchConfig } from '../../../../platform/terminal/common/terminal.js'; +import { TerminalCapability } from '../../../../platform/terminal/common/capabilities/capabilities.js'; +import type { IShellLaunchConfig, ShellIntegrationInjectionFailureReason } from '../../../../platform/terminal/common/terminal.js'; import type { IWorkbenchContribution } from '../../../common/contributions.js'; -import { ITerminalService } from './terminal.js'; +import { ILifecycleService } from '../../../services/lifecycle/common/lifecycle.js'; +import { ITerminalService, type ITerminalInstance } from './terminal.js'; export class TerminalTelemetryContribution extends Disposable implements IWorkbenchContribution { static ID = 'terminalTelemetry'; constructor( + @ILifecycleService lifecycleService: ILifecycleService, @ITerminalService terminalService: ITerminalService, - @ITelemetryService private readonly _telemetryService: ITelemetryService + @ITelemetryService private readonly _telemetryService: ITelemetryService, ) { super(); this._register(terminalService.onDidCreateInstance(async instance => { - // Wait for process ready so the shell launch config is fully resolved - await instance.processReady; - this._logCreateInstance(instance.shellLaunchConfig); + const store = new DisposableStore(); + this._store.add(store); + + await Promise.race([ + // Wait for process ready so the shell launch config is fully resolved, then + // allow another 10 seconds for the shell integration to be fully initialized + instance.processReady.then(() => { + return timeout(10000); + }), + // If the terminal is disposed, it's ready to report on immediately + Event.toPromise(instance.onDisposed, store), + // If the app is shutting down, flush + Event.toPromise(lifecycleService.onWillShutdown, store), + ]); + + this._logCreateInstance(instance); + this._store.delete(store); })); } - private _logCreateInstance(shellLaunchConfig: IShellLaunchConfig): void { + private _logCreateInstance(instance: ITerminalInstance): void { + const slc = instance.shellLaunchConfig; + const commandDetection = instance.capabilities.get(TerminalCapability.CommandDetection); + type TerminalCreationTelemetryData = { shellType: string; - isReconnect: boolean; + promptType: string | undefined; + isCustomPtyImplementation: boolean; + isExtensionOwnedTerminal: boolean; isLoginShell: boolean; + isReconnect: boolean; + + shellIntegrationQuality: number; + shellIntegrationInjected: boolean; + shellIntegrationInjectionFailureReason: ShellIntegrationInjectionFailureReason | undefined; }; type TerminalCreationTelemetryClassification = { owner: 'tyriar'; comment: 'Track details about terminal creation, such as the shell type'; - shellType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The path of the file as a hash.' }; - isReconnect: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the terminal is reconnecting to an existing instance.' }; + + shellType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The detected shell type for the terminal.' }; + promptType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The detected prompt type for the terminal.' }; + isCustomPtyImplementation: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the terminal was using a custom PTY implementation.' }; + isExtensionOwnedTerminal: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the terminal was created by an extension.' }; isLoginShell: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the arguments contain -l or --login.' }; + isReconnect: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the terminal is reconnecting to an existing instance.' }; + + shellIntegrationQuality: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The shell integration quality (rich=2, basic=1 or none=0).' }; + shellIntegrationInjected: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the shell integration script was injected.' }; + shellIntegrationInjectionFailureReason: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Info about shell integration injection.' }; }; this._telemetryService.publicLog2('terminal/createInstance', { - shellType: getSanitizedShellType(shellLaunchConfig), - isReconnect: !!shellLaunchConfig.attachPersistentProcess, - isCustomPtyImplementation: !!shellLaunchConfig.customPtyImplementation, - isLoginShell: (typeof shellLaunchConfig.args === 'string' ? shellLaunchConfig.args.split(' ') : shellLaunchConfig.args)?.some(arg => arg === '-l' || arg === '--login') ?? false, + shellType: getSanitizedShellType(slc), + promptType: commandDetection?.promptType, + + isCustomPtyImplementation: !!slc.customPtyImplementation, + isExtensionOwnedTerminal: !!slc.isExtensionOwnedTerminal, + isLoginShell: (typeof slc.args === 'string' ? slc.args.split(' ') : slc.args)?.some(arg => arg === '-l' || arg === '--login') ?? false, + isReconnect: !!slc.attachPersistentProcess, + + shellIntegrationQuality: commandDetection?.hasRichCommandDetection ? 2 : commandDetection ? 1 : 0, + shellIntegrationInjected: instance.usedShellIntegrationInjection, + shellIntegrationInjectionFailureReason: instance.shellIntegrationInjectionFailureReason, }); } } +// #region Shell Type + const enum AllowedShellType { Unknown = 'unknown', // Windows only CommandPrompt = 'cmd', + Cygwin = 'cygwin-bash', GitBash = 'git-bash', + Msys2 = 'msys2-bash', WindowsPowerShell = 'windows-powershell', Wsl = 'wsl', - // All platforms + + // Common Unix shells Bash = 'bash', - Csh = 'csh', - Dash = 'dash', Fish = 'fish', - Ksh = 'ksh', - Nushell = 'nu', Pwsh = 'pwsh', + PwshPreview = 'pwsh-preview', Sh = 'sh', Ssh = 'ssh', - Tcsh = 'tcsh', Tmux = 'tmux', Zsh = 'zsh', + // More shells + Amm = 'amm', + Ash = 'ash', + Csh = 'csh', + Dash = 'dash', + Elvish = 'elvish', + Ion = 'ion', + Ksh = 'ksh', + Mksh = 'mksh', + Msh = 'msh', + NuShell = 'nu', + Plan9Shell = 'rc', + SchemeShell = 'scsh', + Tcsh = 'tcsh', + Termux = 'termux', + Xonsh = 'xonsh', + // Lanugage REPLs + // These are expected to be very low since they are not typically the default shell + Clojure = 'clj', + CommonLispSbcl = 'sbcl', + Crystal = 'crystal', + Deno = 'deno', + Elixir = 'iex', + Erlang = 'erl', + FSharp = 'fsi', + Go = 'go', + HaskellGhci = 'ghci', + Java = 'jshell', Julia = 'julia', + Lua = 'lua', Node = 'node', + Ocaml = 'ocaml', + Perl = 'perl', + Php = 'php', + PrologSwipl = 'swipl', Python = 'python', + R = 'R', RubyIrb = 'irb', + Scala = 'scala', + SchemeRacket = 'racket', + SmalltalkGnu = 'gst', + SmalltalkPharo = 'pharo', + Tcl = 'tclsh', + TsNode = 'ts-node', } // Types that match the executable name directly const shellTypeExecutableAllowList: Set = new Set([ + // Windows only AllowedShellType.CommandPrompt, AllowedShellType.Wsl, + // Common Unix shells AllowedShellType.Bash, - AllowedShellType.Csh, - AllowedShellType.Dash, AllowedShellType.Fish, - AllowedShellType.Ksh, - AllowedShellType.Nushell, AllowedShellType.Pwsh, AllowedShellType.Sh, AllowedShellType.Ssh, - AllowedShellType.Tcsh, AllowedShellType.Tmux, AllowedShellType.Zsh, + // More shells + AllowedShellType.Amm, + AllowedShellType.Ash, + AllowedShellType.Csh, + AllowedShellType.Dash, + AllowedShellType.Elvish, + AllowedShellType.Ion, + AllowedShellType.Ksh, + AllowedShellType.Mksh, + AllowedShellType.Msh, + AllowedShellType.NuShell, + AllowedShellType.Plan9Shell, + AllowedShellType.SchemeShell, + AllowedShellType.Tcsh, + AllowedShellType.Termux, + AllowedShellType.Xonsh, + + // Lanugage REPLs + AllowedShellType.Clojure, + AllowedShellType.CommonLispSbcl, + AllowedShellType.Crystal, + AllowedShellType.Deno, + AllowedShellType.Elixir, + AllowedShellType.Erlang, + AllowedShellType.FSharp, + AllowedShellType.Go, + AllowedShellType.HaskellGhci, + AllowedShellType.Java, AllowedShellType.Julia, + AllowedShellType.Lua, AllowedShellType.Node, + AllowedShellType.Ocaml, + AllowedShellType.Perl, + AllowedShellType.Php, + AllowedShellType.PrologSwipl, + AllowedShellType.Python, + AllowedShellType.R, AllowedShellType.RubyIrb, + AllowedShellType.Scala, + AllowedShellType.SchemeRacket, + AllowedShellType.SmalltalkGnu, + AllowedShellType.SmalltalkPharo, + AllowedShellType.Tcl, + AllowedShellType.TsNode, ]) satisfies Set; // Dynamic executables that map to a single type const shellTypeExecutableRegexAllowList: { regex: RegExp; type: AllowedShellType }[] = [ + { regex: /^(?:pwsh|powershell)-preview$/i, type: AllowedShellType.PwshPreview }, { regex: /^python(?:\d+(?:\.\d+)?)?$/i, type: AllowedShellType.Python }, ]; // Path-based look ups const shellTypePathRegexAllowList: { regex: RegExp; type: AllowedShellType }[] = [ + // Cygwin uses bash.exe, so look up based on the path + { regex: /\\Cygwin(?:64)?\\.+\\bash\.exe$/i, type: AllowedShellType.Cygwin }, // Git bash uses bash.exe, so look up based on the path - { regex: /Git\\bin\\bash\.exe$/i, type: AllowedShellType.GitBash }, + { regex: /\\Git\\.+\\bash\.exe$/i, type: AllowedShellType.GitBash }, + // Msys2 uses bash.exe, so look up based on the path + { regex: /\\msys(?:32|64)\\.+\\(?:bash|msys2)\.exe$/i, type: AllowedShellType.Msys2 }, // WindowsPowerShell should always be installed on this path, we cannot just look at the // executable name since powershell is the CLI on other platforms sometimes (eg. snap package) - { regex: /WindowsPowerShell\\v1.0\\powershell.exe$/i, type: AllowedShellType.WindowsPowerShell }, + { regex: /\\WindowsPowerShell\\v1.0\\powershell.exe$/i, type: AllowedShellType.WindowsPowerShell }, // WSL executables will represent some other shell in the end, but it's difficult to determine // when we log - { regex: /Windows\\System32\\(?:bash|wsl)\.exe$/i, type: AllowedShellType.Wsl }, + { regex: /\\Windows\\(?:System32|SysWOW64|Sysnative)\\(?:bash|wsl)\.exe$/i, type: AllowedShellType.Wsl }, ]; -function getSanitizedShellType(shellLaunchConfig: IShellLaunchConfig): AllowedShellType { - if (!shellLaunchConfig.executable) { +function getSanitizedShellType(slc: IShellLaunchConfig): AllowedShellType { + if (!slc.executable) { return AllowedShellType.Unknown; } - const executableFile = basename(shellLaunchConfig.executable); + const executableFile = basename(slc.executable); const executableFileWithoutExt = executableFile.replace(/\.[^\.]+$/, ''); for (const entry of shellTypePathRegexAllowList) { - if (entry.regex.test(shellLaunchConfig.executable)) { + if (entry.regex.test(slc.executable)) { return entry.type; } } @@ -141,3 +269,5 @@ function getSanitizedShellType(shellLaunchConfig: IShellLaunchConfig): AllowedSh } return AllowedShellType.Unknown; } + +// #endregion Shell Type diff --git a/src/vs/workbench/contrib/terminal/common/basePty.ts b/src/vs/workbench/contrib/terminal/common/basePty.ts index 9a8c11ce3ffc..a851f56225e0 100644 --- a/src/vs/workbench/contrib/terminal/common/basePty.ts +++ b/src/vs/workbench/contrib/terminal/common/basePty.ts @@ -25,7 +25,8 @@ export abstract class BasePty extends Disposable implements Partial Promise; @@ -56,6 +57,7 @@ export class TerminalLinkManager extends DisposableStore { @IConfigurationService private readonly _configurationService: IConfigurationService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @INotificationService notificationService: INotificationService, + @ITelemetryService private readonly _telemetryService: ITelemetryService, @ITerminalConfigurationService terminalConfigurationService: ITerminalConfigurationService, @ITerminalLogService private readonly _logService: ITerminalLogService, @ITunnelService private readonly _tunnelService: ITunnelService, @@ -191,6 +193,13 @@ export class TerminalLinkManager extends DisposableStore { if (!opener) { throw new Error(`No matching opener for link type "${link.type}"`); } + this._telemetryService.publicLog2<{ + linkType: TerminalBuiltinLinkType | string; + }, { + owner: 'tyriar'; + comment: 'When the user opens a link in the terminal'; + linkType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The type of link being opened' }; + }>('terminal/openLink', { linkType: typeof link.type === 'string' ? link.type : `extension:${link.type.id}` }); await opener.open(link); } diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestTelemetry.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestTelemetry.ts index 4eef8c24066a..8f89b84fa049 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestTelemetry.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestTelemetry.ts @@ -58,19 +58,19 @@ export class TerminalSuggestTelemetry extends Disposable { return; } - let outcome: string; + let completionOutcome: string; if (fromInterrupt) { - outcome = CompletionOutcome.Interrupted; + completionOutcome = CompletionOutcome.Interrupted; } else if (commandLine.trim() && commandLine.includes(label)) { - outcome = CompletionOutcome.Accepted; + completionOutcome = CompletionOutcome.Accepted; } else if (inputContainsFirstHalfOfLabel(commandLine, label)) { - outcome = CompletionOutcome.AcceptedWithEdit; + completionOutcome = CompletionOutcome.AcceptedWithEdit; } else { - outcome = CompletionOutcome.Deleted; + completionOutcome = CompletionOutcome.Deleted; } this._telemetryService.publicLog2<{ kind: string | undefined; - outcome: string; + completionOutcome: string; exitCode: number | undefined; }, { owner: 'meganrogge'; @@ -80,10 +80,10 @@ export class TerminalSuggestTelemetry extends Disposable { purpose: 'FeatureInsight'; comment: 'The completion item\'s kind'; }; - outcome: { + completionOutcome: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; - comment: 'The outcome of the accepted completion'; + comment: 'The outcome of the completion'; }; exitCode: { classification: 'SystemMetaData'; @@ -92,7 +92,7 @@ export class TerminalSuggestTelemetry extends Disposable { }; }>('terminal.suggest.acceptedCompletion', { kind, - outcome, + completionOutcome, exitCode }); } diff --git a/src/vs/workbench/contrib/terminalContrib/typeAhead/browser/terminalTypeAheadAddon.ts b/src/vs/workbench/contrib/terminalContrib/typeAhead/browser/terminalTypeAheadAddon.ts index e286592242db..4dfe8aec91b8 100644 --- a/src/vs/workbench/contrib/terminalContrib/typeAhead/browser/terminalTypeAheadAddon.ts +++ b/src/vs/workbench/contrib/terminalContrib/typeAhead/browser/terminalTypeAheadAddon.ts @@ -1290,8 +1290,8 @@ export const enum CharPredictState { export class TypeAheadAddon extends Disposable implements ITerminalAddon { private _typeaheadStyle?: TypeAheadStyle; - private _typeaheadThreshold = this._configurationService.getValue(TERMINAL_CONFIG_SECTION).localEchoLatencyThreshold; - private _excludeProgramRe = compileExcludeRegexp(this._configurationService.getValue(TERMINAL_CONFIG_SECTION).localEchoExcludePrograms); + private _typeaheadThreshold: number; + private _excludeProgramRe: RegExp; protected _lastRow?: { y: number; startingX: number; endingX: number; charState: CharPredictState }; protected _timeline?: PredictionTimeline; private _terminalTitle = ''; @@ -1308,6 +1308,8 @@ export class TypeAheadAddon extends Disposable implements ITerminalAddon { @ITelemetryService private readonly _telemetryService: ITelemetryService, ) { super(); + this._typeaheadThreshold = this._configurationService.getValue(TERMINAL_CONFIG_SECTION).localEchoLatencyThreshold; + this._excludeProgramRe = compileExcludeRegexp(this._configurationService.getValue(TERMINAL_CONFIG_SECTION).localEchoExcludePrograms); this._register(toDisposable(() => this._clearPredictionDebounce?.dispose())); } diff --git a/src/vs/workbench/contrib/testing/browser/explorerProjections/index.ts b/src/vs/workbench/contrib/testing/browser/explorerProjections/index.ts index 552bf1a22989..b781d5b157a8 100644 --- a/src/vs/workbench/contrib/testing/browser/explorerProjections/index.ts +++ b/src/vs/workbench/contrib/testing/browser/explorerProjections/index.ts @@ -74,7 +74,7 @@ export abstract class TestItemTreeElement { /** * Depth of the element in the tree. */ - public depth: number = this.parent ? this.parent.depth + 1 : 0; + public depth: number; /** * Whether the node's test result is 'retired' -- from an outdated test run. @@ -104,7 +104,9 @@ export abstract class TestItemTreeElement { * in a 'flat' projection. */ public readonly parent: TestItemTreeElement | null = null, - ) { } + ) { + this.depth = parent ? parent.depth + 1 : 0; + } public toJSON() { if (this.depth === 0) { diff --git a/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsOutput.ts b/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsOutput.ts index 4f06c5ee9ca5..ce9d447356fe 100644 --- a/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsOutput.ts +++ b/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsOutput.ts @@ -41,17 +41,20 @@ import { ITaskRawOutput, ITestResult, ITestRunTaskResults, LiveTestResult, TestR import { ITestMessage, TestMessageType, getMarkId } from '../../common/testTypes.js'; import { ScrollEvent } from '../../../../../base/common/scrollable.js'; import { CALL_STACK_WIDGET_HEADER_HEIGHT } from '../../../debug/browser/callStackWidget.js'; +import { ITextModel } from '../../../../../editor/common/model.js'; class SimpleDiffEditorModel extends EditorModel { - public readonly original = this._original.object.textEditorModel; - public readonly modified = this._modified.object.textEditorModel; + public readonly original: ITextModel; + public readonly modified: ITextModel; constructor( private readonly _original: IReference, private readonly _modified: IReference, ) { super(); + this.original = this._original.object.textEditorModel; + this.modified = this._modified.object.textEditorModel; } public override dispose() { diff --git a/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsTree.ts b/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsTree.ts index 4915553ce612..db996a3ed2f4 100644 --- a/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsTree.ts +++ b/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsTree.ts @@ -79,9 +79,9 @@ class TestResultElement implements ITreeElement { public readonly changeEmitter = new Emitter(); public readonly onDidChange = this.changeEmitter.event; public readonly type = 'result'; - public readonly context = this.value.id; - public readonly id = this.value.id; - public readonly label = this.value.name; + public readonly context: string; + public readonly id: string; + public readonly label: string; public get icon() { return icons.testingStatesToIcons.get( @@ -91,7 +91,11 @@ class TestResultElement implements ITreeElement { ); } - constructor(public readonly value: ITestResult) { } + constructor(public readonly value: ITestResult) { + this.id = value.id; + this.context = value.id; + this.label = value.name; + } } const openCoverageLabel = localize('openTestCoverage', 'View Test Coverage'); @@ -100,7 +104,7 @@ const closeCoverageLabel = localize('closeTestCoverage', 'Close Test Coverage'); class CoverageElement implements ITreeElement { public readonly type = 'coverage'; public readonly context: undefined; - public readonly id = `coverage-${this.results.id}/${this.task.id}`; + public readonly id: string; public readonly onDidChange: Event; public get label() { @@ -116,10 +120,11 @@ class CoverageElement implements ITreeElement { } constructor( - private readonly results: ITestResult, + results: ITestResult, public readonly task: ITestRunTaskResults, private readonly coverageService: ITestCoverageService, ) { + this.id = `coverage-${results.id}/${task.id}`; this.onDidChange = Event.fromObservableLight(coverageService.selected); } } @@ -127,23 +132,23 @@ class CoverageElement implements ITreeElement { class OlderResultsElement implements ITreeElement { public readonly type = 'older'; public readonly context: undefined; - public readonly id = `older-${this.n}`; + public readonly id: string; public readonly onDidChange = Event.None; public readonly label: string; constructor(private readonly n: number) { - this.label = localize('nOlderResults', '{0} older results', n); + this.label = n === 1 + ? localize('oneOlderResult', '1 older result') + : localize('nOlderResults', '{0} older results', n); + this.id = `older-${this.n}`; } } class TestCaseElement implements ITreeElement { public readonly type = 'test'; - public readonly context: ITestItemContext = { - $mid: MarshalledId.TestItemContext, - tests: [InternalTestItem.serialize(this.test)], - }; - public readonly id = `${this.results.id}/${this.test.item.extId}`; + public readonly context: ITestItemContext; + public readonly id: string; public readonly description?: string; public get onDidChange() { @@ -179,7 +184,13 @@ class TestCaseElement implements ITreeElement { public readonly results: ITestResult, public readonly test: TestResultItem, public readonly taskIndex: number, - ) { } + ) { + this.id = `${results.id}/${test.item.extId}`; + this.context = { + $mid: MarshalledId.TestItemContext, + tests: [InternalTestItem.serialize(test)], + }; + } } class TaskElement implements ITreeElement { @@ -832,6 +843,14 @@ class TreeActionsProvider { ...getTestItemContextOverlay(element.test, capabilities), ); + primary.push(new Action( + 'testing.outputPeek.goToTest', + localize('testing.goToTest', "Go to Test"), + ThemeIcon.asClassName(Codicon.goToFile), + undefined, + () => this.commandService.executeCommand('vscode.revealTest', element.test.item.extId), + )); + const extId = element.test.item.extId; if (element.test.tasks[element.taskIndex].messages.some(m => m.type === TestMessageType.Output)) { primary.push(new Action( @@ -877,14 +896,6 @@ class TreeActionsProvider { id = MenuId.TestMessageContext; contextKeys.push([TestingContextKeys.testMessageContext.key, element.contextValue]); - primary.push(new Action( - 'testing.outputPeek.goToTest', - localize('testing.goToTest', "Go to Test"), - ThemeIcon.asClassName(Codicon.goToFile), - undefined, - () => this.commandService.executeCommand('vscode.revealTest', element.test.item.extId), - )); - if (this.showRevealLocationOnMessages && element.location) { primary.push(new Action( 'testing.outputPeek.goToError', diff --git a/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts b/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts index 0894eeca52b5..6e08a8843f48 100644 --- a/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts +++ b/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts @@ -37,11 +37,7 @@ export class TestingExplorerFilter extends BaseActionViewItem { private wrapper!: HTMLDivElement; private readonly focusEmitter = this._register(new Emitter()); public readonly onDidFocus = this.focusEmitter.event; - private readonly history: StoredValue<{ values: string[]; lastValue: string } | string[]> = this._register(this.instantiationService.createInstance(StoredValue, { - key: 'testing.filterHistory2', - scope: StorageScope.WORKSPACE, - target: StorageTarget.MACHINE - })); + private readonly history: StoredValue<{ values: string[]; lastValue: string } | string[]>; private readonly filtersAction = new Action('markersFiltersAction', localize('testing.filters.menu', "More Filters..."), 'testing-filter-button ' + ThemeIcon.asClassName(testingFilterIcon)); @@ -53,6 +49,11 @@ export class TestingExplorerFilter extends BaseActionViewItem { @ITestService private readonly testService: ITestService, ) { super(null, action, options); + this.history = this._register(instantiationService.createInstance(StoredValue, { + key: 'testing.filterHistory2', + scope: StorageScope.WORKSPACE, + target: StorageTarget.MACHINE + })); this.updateFilterActiveState(); this._register(testService.excluded.onTestExclusionsChanged(this.updateFilterActiveState, this)); } diff --git a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts index 421f9578df87..a3c11d2405ba 100644 --- a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts +++ b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts @@ -36,7 +36,7 @@ import { MenuEntryActionViewItem, createActionViewItem, getActionBarActions, get import { IMenuService, MenuId, MenuItemAction } from '../../../../platform/actions/common/actions.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { IContextKey, 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'; @@ -701,15 +701,11 @@ class TestingExplorerViewModel extends Disposable { public readonly projection = this._register(new MutableDisposable()); private readonly revealTimeout = new MutableDisposable(); - private readonly _viewMode = TestingContextKeys.viewMode.bindTo(this.contextKeyService); - private readonly _viewSorting = TestingContextKeys.viewSorting.bindTo(this.contextKeyService); + private readonly _viewMode: IContextKey; + private readonly _viewSorting: IContextKey; private readonly welcomeVisibilityEmitter = new Emitter(); private readonly actionRunner = this._register(new TestExplorerActionRunner(() => this.tree.getSelection().filter(isDefined))); - private readonly lastViewState = this._register(new StoredValue({ - key: 'testing.treeState', - scope: StorageScope.WORKSPACE, - target: StorageTarget.MACHINE, - }, this.storageService)); + private readonly lastViewState: StoredValue; private readonly noTestForDocumentWidget: NoTestsForDocumentWidget; /** @@ -781,6 +777,13 @@ class TestingExplorerViewModel extends Disposable { this.hasPendingReveal = !!filterState.reveal.get(); this.noTestForDocumentWidget = this._register(instantiationService.createInstance(NoTestsForDocumentWidget, listContainer)); + this.lastViewState = this._register(new StoredValue({ + key: 'testing.treeState', + scope: StorageScope.WORKSPACE, + target: StorageTarget.MACHINE, + }, this.storageService)); + this._viewMode = TestingContextKeys.viewMode.bindTo(contextKeyService); + this._viewSorting = TestingContextKeys.viewSorting.bindTo(contextKeyService); this._viewMode.set(this.storageService.get('testing.viewMode', StorageScope.WORKSPACE, TestExplorerViewMode.Tree) as TestExplorerViewMode); this._viewSorting.set(this.storageService.get('testing.viewSorting', StorageScope.WORKSPACE, TestExplorerViewSorting.ByLocation) as TestExplorerViewSorting); @@ -1545,6 +1548,12 @@ class TestItemRenderer extends Disposable : undefined })); + disposable.add(this.profiles.onDidChange(() => { + if (templateData.current) { + this.fillActionBar(templateData.current, templateData); + } + })); + disposable.add(this.crService.onDidChange(changed => { const id = templateData.current?.test.item.extId; if (id && (!changed || changed === id || TestId.isChild(id, changed))) { diff --git a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts index bb40133b638e..788ed44ccd02 100644 --- a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts +++ b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts @@ -116,11 +116,7 @@ export class TestingPeekOpener extends Disposable implements ITestingPeekOpener private lastUri?: TestUriWithDocument; /** @inheritdoc */ - public readonly historyVisible = this._register(MutableObservableValue.stored(new StoredValue({ - key: 'testHistoryVisibleInPeek', - scope: StorageScope.PROFILE, - target: StorageTarget.USER, - }, this.storageService), false)); + public readonly historyVisible: MutableObservableValue; constructor( @IConfigurationService private readonly configuration: IConfigurationService, @@ -128,13 +124,18 @@ export class TestingPeekOpener extends Disposable implements ITestingPeekOpener @ICodeEditorService private readonly codeEditorService: ICodeEditorService, @ITestResultService private readonly testResults: ITestResultService, @ITestService private readonly testService: ITestService, - @IStorageService private readonly storageService: IStorageService, + @IStorageService storageService: IStorageService, @IViewsService private readonly viewsService: IViewsService, @ICommandService private readonly commandService: ICommandService, @INotificationService private readonly notificationService: INotificationService, ) { super(); this._register(testResults.onTestChanged(this.openPeekOnFailure, this)); + this.historyVisible = this._register(MutableObservableValue.stored(new StoredValue({ + key: 'testHistoryVisibleInPeek', + scope: StorageScope.PROFILE, + target: StorageTarget.USER, + }, storageService), false)); } /** @inheritdoc */ diff --git a/src/vs/workbench/contrib/testing/common/testExclusions.ts b/src/vs/workbench/contrib/testing/common/testExclusions.ts index 5ac06c15fc1d..408c8975b906 100644 --- a/src/vs/workbench/contrib/testing/common/testExclusions.ts +++ b/src/vs/workbench/contrib/testing/common/testExclusions.ts @@ -12,26 +12,28 @@ import { StoredValue } from './storedValue.js'; import { InternalTestItem } from './testTypes.js'; export class TestExclusions extends Disposable { - private readonly excluded = this._register( - MutableObservableValue.stored(new StoredValue>({ - key: 'excludedTestItems', - scope: StorageScope.WORKSPACE, - target: StorageTarget.MACHINE, - serialization: { - deserialize: v => new Set(JSON.parse(v)), - serialize: v => JSON.stringify([...v]) - }, - }, this.storageService), new Set()) - ); + private readonly excluded: MutableObservableValue>; constructor(@IStorageService private readonly storageService: IStorageService) { super(); + this.excluded = this._register( + MutableObservableValue.stored(new StoredValue>({ + key: 'excludedTestItems', + scope: StorageScope.WORKSPACE, + target: StorageTarget.MACHINE, + serialization: { + deserialize: v => new Set(JSON.parse(v)), + serialize: v => JSON.stringify([...v]) + }, + }, this.storageService), new Set()) + ); + this.onTestExclusionsChanged = this.excluded.onDidChange; } /** * Event that fires when the excluded tests change. */ - public readonly onTestExclusionsChanged: Event = this.excluded.onDidChange; + public readonly onTestExclusionsChanged: Event; /** * Gets whether there's any excluded tests. diff --git a/src/vs/workbench/contrib/testing/common/testExplorerFilterState.ts b/src/vs/workbench/contrib/testing/common/testExplorerFilterState.ts index 1d865c3a37d0..9c187e33e76e 100644 --- a/src/vs/workbench/contrib/testing/common/testExplorerFilterState.ts +++ b/src/vs/workbench/contrib/testing/common/testExplorerFilterState.ts @@ -99,11 +99,7 @@ export class TestExplorerFilterState extends Disposable implements ITestExplorer public readonly text = this._register(new MutableObservableValue('')); /** @inheritdoc */ - public readonly fuzzy = this._register(MutableObservableValue.stored(new StoredValue({ - key: 'testHistoryFuzzy', - scope: StorageScope.PROFILE, - target: StorageTarget.USER, - }, this.storageService), false)); + public readonly fuzzy: MutableObservableValue; public readonly reveal: ISettableObservable = observableValue('TestExplorerFilterState.reveal', undefined); @@ -113,9 +109,14 @@ export class TestExplorerFilterState extends Disposable implements ITestExplorer public readonly onDidSelectTestInExplorer = this.selectTestInExplorerEmitter.event; constructor( - @IStorageService private readonly storageService: IStorageService, + @IStorageService storageService: IStorageService, ) { super(); + this.fuzzy = this._register(MutableObservableValue.stored(new StoredValue({ + key: 'testHistoryFuzzy', + scope: StorageScope.PROFILE, + target: StorageTarget.USER, + }, storageService), false)); } /** @inheritdoc */ diff --git a/src/vs/workbench/contrib/testing/common/testResultStorage.ts b/src/vs/workbench/contrib/testing/common/testResultStorage.ts index 2253712efb8d..6b8ee0d1a63d 100644 --- a/src/vs/workbench/contrib/testing/common/testResultStorage.ts +++ b/src/vs/workbench/contrib/testing/common/testResultStorage.ts @@ -49,18 +49,19 @@ const currentRevision = 1; export abstract class BaseTestResultStorage extends Disposable implements ITestResultStorage { declare readonly _serviceBrand: undefined; - protected readonly stored = this._register(new StoredValue>({ - key: 'storedTestResults', - scope: StorageScope.WORKSPACE, - target: StorageTarget.MACHINE - }, this.storageService)); + protected readonly stored: StoredValue>; constructor( @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, - @IStorageService private readonly storageService: IStorageService, + @IStorageService storageService: IStorageService, @ILogService private readonly logService: ILogService, ) { super(); + this.stored = this._register(new StoredValue>({ + key: 'storedTestResults', + scope: StorageScope.WORKSPACE, + target: StorageTarget.MACHINE + }, storageService)); } /** diff --git a/src/vs/workbench/contrib/testing/common/testServiceImpl.ts b/src/vs/workbench/contrib/testing/common/testServiceImpl.ts index 00ea40df0f78..67febf8cb4a3 100644 --- a/src/vs/workbench/contrib/testing/common/testServiceImpl.ts +++ b/src/vs/workbench/contrib/testing/common/testServiceImpl.ts @@ -72,7 +72,7 @@ export class TestService extends Disposable implements ITestService { /** * @inheritdoc */ - public readonly collection = new MainThreadTestCollection(this.uriIdentityService, this.expandTest.bind(this)); + public readonly collection: MainThreadTestCollection; /** * @inheritdoc @@ -82,17 +82,13 @@ export class TestService extends Disposable implements ITestService { /** * @inheritdoc */ - public readonly showInlineOutput = this._register(MutableObservableValue.stored(new StoredValue({ - key: 'inlineTestOutputVisible', - scope: StorageScope.WORKSPACE, - target: StorageTarget.USER - }, this.storage), true)); + public readonly showInlineOutput: MutableObservableValue; constructor( @IContextKeyService contextKeyService: IContextKeyService, @IInstantiationService instantiationService: IInstantiationService, - @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, - @IStorageService private readonly storage: IStorageService, + @IUriIdentityService uriIdentityService: IUriIdentityService, + @IStorageService storage: IStorageService, @IEditorService private readonly editorService: IEditorService, @ITestProfileService private readonly testProfiles: ITestProfileService, @INotificationService private readonly notificationService: INotificationService, @@ -101,6 +97,13 @@ export class TestService extends Disposable implements ITestService { @IWorkspaceTrustRequestService private readonly workspaceTrustRequestService: IWorkspaceTrustRequestService, ) { super(); + this.collection = new MainThreadTestCollection(uriIdentityService, this.expandTest.bind(this)); + this.showInlineOutput = this._register(MutableObservableValue.stored(new StoredValue({ + key: 'inlineTestOutputVisible', + scope: StorageScope.WORKSPACE, + target: StorageTarget.USER + }, storage), true)); + this.excluded = instantiationService.createInstance(TestExclusions); this.isRefreshingTests = TestingContextKeys.isRefreshingTests.bindTo(contextKeyService); this.activeEditorHasTests = TestingContextKeys.activeEditorHasTests.bindTo(contextKeyService); diff --git a/src/vs/workbench/contrib/testing/common/testingContinuousRunService.ts b/src/vs/workbench/contrib/testing/common/testingContinuousRunService.ts index 6e0e61681117..fec6792ad2d1 100644 --- a/src/vs/workbench/contrib/testing/common/testingContinuousRunService.ts +++ b/src/vs/workbench/contrib/testing/common/testingContinuousRunService.ts @@ -7,8 +7,7 @@ import * as arrays from '../../../../base/common/arrays.js'; import { CancellationTokenSource } from '../../../../base/common/cancellation.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { Disposable, DisposableMap, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; -import { ISettableObservable, observableValue } from '../../../../base/common/observable.js'; -import { autorunIterableDelta } from '../../../../base/common/observableInternal/autorun.js'; +import { autorunIterableDelta, ISettableObservable, observableValue } from '../../../../base/common/observable.js'; import { WellDefinedPrefixTree } from '../../../../base/common/prefixTree.js'; import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; diff --git a/src/vs/workbench/contrib/testing/test/common/testStubs.ts b/src/vs/workbench/contrib/testing/test/common/testStubs.ts index d484c5e4f629..f121bd9cd830 100644 --- a/src/vs/workbench/contrib/testing/test/common/testStubs.ts +++ b/src/vs/workbench/contrib/testing/test/common/testStubs.ts @@ -7,7 +7,7 @@ import { URI } from '../../../../../base/common/uri.js'; import { MainThreadTestCollection } from '../../common/mainThreadTestCollection.js'; import { ITestItem, TestsDiff } from '../../common/testTypes.js'; import { TestId } from '../../common/testId.js'; -import { createTestItemChildren, ITestItemApi, ITestItemLike, TestItemCollection, TestItemEventOp } from '../../common/testItemCollection.js'; +import { createTestItemChildren, ITestItemApi, ITestItemChildren, ITestItemLike, TestItemCollection, TestItemEventOp } from '../../common/testItemCollection.js'; export class TestTestItem implements ITestItemLike { private readonly props: ITestItem; @@ -39,15 +39,17 @@ export class TestTestItem implements ITestItemLike { return this._extId.localId; } - public api: ITestItemApi = { controllerId: this._extId.controllerId }; + public api: ITestItemApi; - public children = createTestItemChildren(this.api, i => i.api, TestTestItem); + public children: ITestItemChildren; constructor( private readonly _extId: TestId, label: string, uri?: URI, ) { + this.api = { controllerId: this._extId.controllerId }; + this.children = createTestItemChildren(this.api, i => i.api, TestTestItem); this.props = { extId: _extId.toString(), busy: false, diff --git a/src/vs/workbench/contrib/webview/browser/pre/index-no-csp.html b/src/vs/workbench/contrib/webview/browser/pre/index-no-csp.html index 9221d984af0e..309df85a98bc 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/index-no-csp.html +++ b/src/vs/workbench/contrib/webview/browser/pre/index-no-csp.html @@ -230,7 +230,7 @@ return reject(new Error('Service Workers are not enabled. Webviews will not work. Try disabling private/incognito mode.')); } - const swPath = encodeURI(`service-worker.js?v=${expectedWorkerVersion}&vscode-resource-base-authority=${searchParams.get('vscode-resource-base-authority')}&remoteAuthority=${searchParams.get('remoteAuthority') ?? ''}`); + const swPath = encodeURI(`service-worker.js?v=${expectedWorkerVersion}&vscode-resource-base-authority=${searchParams.get('vscode-resource-base-authority')}&id=${ID}&remoteAuthority=${searchParams.get('remoteAuthority') ?? ''}`); navigator.serviceWorker.register(swPath) .then(async registration => { /** @@ -259,7 +259,7 @@ navigator.serviceWorker.addEventListener('message', versionHandler); const postVersionMessage = (/** @type {ServiceWorker} */ controller) => { - controller.postMessage({ channel: 'version', data: { webviewID: ID } }); + controller.postMessage({ channel: 'version' }); }; // At this point, either the service worker is ready and diff --git a/src/vs/workbench/contrib/webview/browser/pre/index.html b/src/vs/workbench/contrib/webview/browser/pre/index.html index 837116e2a13e..e123bb924325 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/index.html +++ b/src/vs/workbench/contrib/webview/browser/pre/index.html @@ -5,7 +5,7 @@ + content="default-src 'none'; script-src 'sha256-nlLyDpnjtftJG2xvXh2vuy77l7xFTjfOz7Jnj1iXNmA=' 'self'; frame-src 'self'; style-src 'unsafe-inline';"> @@ -236,7 +236,7 @@ return reject(new Error('Service Workers are not enabled. Webviews will not work. Try disabling private/incognito mode.')); } - const swPath = encodeURI(`service-worker.js?v=${expectedWorkerVersion}&vscode-resource-base-authority=${searchParams.get('vscode-resource-base-authority')}&remoteAuthority=${searchParams.get('remoteAuthority') ?? ''}`); + const swPath = encodeURI(`service-worker.js?v=${expectedWorkerVersion}&vscode-resource-base-authority=${searchParams.get('vscode-resource-base-authority')}&id=${ID}&remoteAuthority=${searchParams.get('remoteAuthority') ?? ''}`); navigator.serviceWorker.register(swPath) .then(async registration => { /** @@ -265,7 +265,7 @@ navigator.serviceWorker.addEventListener('message', versionHandler); const postVersionMessage = (/** @type {ServiceWorker} */ controller) => { - controller.postMessage({ channel: 'version', data: { webviewID: ID } }); + controller.postMessage({ channel: 'version' }); }; // At this point, either the service worker is ready and diff --git a/src/vs/workbench/contrib/webview/browser/pre/service-worker.js b/src/vs/workbench/contrib/webview/browser/pre/service-worker.js index 09a767b06d9a..3d74103ec243 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/service-worker.js +++ b/src/vs/workbench/contrib/webview/browser/pre/service-worker.js @@ -18,7 +18,7 @@ const searchParams = new URL(location.toString()).searchParams; const remoteAuthority = searchParams.get('remoteAuthority'); -let ID = ''; +const ID = searchParams.get('id'); /** * Origin used for resources @@ -135,7 +135,6 @@ sw.addEventListener('message', async (event) => { switch (event.data.channel) { case 'version': { const source = /** @type {Client} */ (event.source); - ID = event.data.webviewID; sw.clients.get(source.id).then(client => { if (client) { client.postMessage({ diff --git a/src/vs/workbench/electron-sandbox/window.ts b/src/vs/workbench/electron-sandbox/window.ts index d306dbe5bdc7..c80a795b4739 100644 --- a/src/vs/workbench/electron-sandbox/window.ts +++ b/src/vs/workbench/electron-sandbox/window.ts @@ -32,7 +32,7 @@ import { IWorkspaceFolderCreationData } from '../../platform/workspaces/common/w import { IIntegrityService } from '../services/integrity/common/integrity.js'; import { isWindows, isMacintosh } from '../../base/common/platform.js'; import { IProductService } from '../../platform/product/common/productService.js'; -import { INotificationService, NeverShowAgainScope, NotificationPriority, Severity } from '../../platform/notification/common/notification.js'; +import { INotificationService, NotificationPriority, Severity } from '../../platform/notification/common/notification.js'; import { IKeybindingService } from '../../platform/keybinding/common/keybinding.js'; import { INativeWorkbenchEnvironmentService } from '../services/environment/electron-sandbox/environmentService.js'; import { IAccessibilityService, AccessibilitySupport } from '../../platform/accessibility/common/accessibility.js'; @@ -728,32 +728,6 @@ export class NativeWindow extends BaseWindow { } } - // macOS 10.15 warning - if (isMacintosh) { - const majorVersion = this.nativeEnvironmentService.os.release.split('.')[0]; - const eolReleases = new Map([ - ['19', 'macOS Catalina'], - ]); - - if (eolReleases.has(majorVersion)) { - const message = localize('macoseolmessage', "{0} on {1} will soon stop receiving updates. Consider upgrading your macOS version.", this.productService.nameLong, eolReleases.get(majorVersion)); - - this.notificationService.prompt( - Severity.Warning, - message, - [{ - label: localize('learnMore', "Learn More"), - run: () => this.openerService.open(URI.parse('https://aka.ms/vscode-faq-old-macOS')) - }], - { - neverShowAgain: { id: 'macoseol', isSecondary: true, scope: NeverShowAgainScope.APPLICATION }, - priority: NotificationPriority.URGENT, - sticky: true - } - ); - } - } - // Slow shell environment progress indicator const shellEnv = process.shellEnv(); this.progressService.withProgress({ diff --git a/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts index 1cde4cdfc682..605a3c09fabb 100644 --- a/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts @@ -255,7 +255,7 @@ class InstallExtensionTask extends AbstractExtensionTask implem readonly identifier: IExtensionIdentifier; readonly source: URI | IGalleryExtension; - private _profileLocation = this.options.profileLocation; + private _profileLocation: URI; get profileLocation() { return this._profileLocation; } private _operation = InstallOperation.Install; @@ -269,6 +269,7 @@ class InstallExtensionTask extends AbstractExtensionTask implem private readonly userDataProfilesService: IUserDataProfilesService, ) { super(); + this._profileLocation = options.profileLocation; this.identifier = URI.isUri(extension) ? { id: getGalleryExtensionId(manifest.publisher, manifest.name) } : extension.identifier; this.source = extension; } diff --git a/src/vs/workbench/services/host/browser/browserHostService.ts b/src/vs/workbench/services/host/browser/browserHostService.ts index 57cb6e482cfc..a35957b80be9 100644 --- a/src/vs/workbench/services/host/browser/browserHostService.ts +++ b/src/vs/workbench/services/host/browser/browserHostService.ts @@ -41,6 +41,7 @@ import { mainWindow, isAuxiliaryWindow } from '../../../../base/browser/window.j import { isIOS, isMacintosh } from '../../../../base/common/platform.js'; import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js'; import { URI } from '../../../../base/common/uri.js'; +import { VSBuffer } from '../../../../base/common/buffer.js'; enum HostShutdownReason { @@ -587,7 +588,7 @@ export class BrowserHostService extends Disposable implements IHostService { //#region Screenshots - async getScreenshot(): Promise { + async getScreenshot(): Promise { // Gets a screenshot from the browser. This gets the screenshot via the browser's display // media API which will typically offer a picker of all available screens and windows for // the user to select. Using the video stream provided by the display media API, this will @@ -633,8 +634,8 @@ export class BrowserHostService extends Disposable implements IHostService { throw new Error('Failed to create blob from canvas'); } - // Convert the Blob to an ArrayBuffer - return blob.arrayBuffer(); + const buf = await blob.bytes(); + return VSBuffer.wrap(buf); } catch (error) { console.error('Error taking screenshot:', error); diff --git a/src/vs/workbench/services/host/browser/host.ts b/src/vs/workbench/services/host/browser/host.ts index 657c5ec4bad6..f7812c386f56 100644 --- a/src/vs/workbench/services/host/browser/host.ts +++ b/src/vs/workbench/services/host/browser/host.ts @@ -128,7 +128,7 @@ export interface IHostService { /** * Captures a screenshot. */ - getScreenshot(): Promise; + getScreenshot(): 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 bda11474b2c3..4d521ef70d53 100644 --- a/src/vs/workbench/services/host/electron-sandbox/nativeHostService.ts +++ b/src/vs/workbench/services/host/electron-sandbox/nativeHostService.ts @@ -193,7 +193,7 @@ class WorkbenchHostService extends Disposable implements IHostService { //#region Screenshots - getScreenshot(): Promise { + getScreenshot(): Promise { return this.nativeHostService.getScreenshot(); } diff --git a/src/vs/workbench/services/progress/browser/progressService.ts b/src/vs/workbench/services/progress/browser/progressService.ts index 8a02c908e63d..cb8831c7e774 100644 --- a/src/vs/workbench/services/progress/browser/progressService.ts +++ b/src/vs/workbench/services/progress/browser/progressService.ts @@ -545,6 +545,7 @@ export class ProgressService extends Disposable implements IProgressService { const disposables = new DisposableStore(); let dialog: Dialog; + let taskCompleted = false; const createDialog = (message: string) => { const buttons = options.buttons || []; @@ -571,8 +572,15 @@ export class ProgressService extends Disposable implements IProgressService { disposables.add(dialog); dialog.show().then(dialogResult => { - onDidCancel?.(dialogResult.button); - + // The dialog may close as a result of disposing it after the + // task has completed. In that case, we do not want to trigger + // the `onDidCancel` callback. + // However, if the task is still running, this means that the + // user has clicked the cancel button and we want to trigger + // the `onDidCancel` callback. + if (!taskCompleted) { + onDidCancel?.(dialogResult.button); + } dispose(dialog); }); @@ -611,6 +619,7 @@ export class ProgressService extends Disposable implements IProgressService { }); promise.finally(() => { + taskCompleted = true; dispose(disposables); }); diff --git a/src/vs/workbench/services/textMate/browser/backgroundTokenization/textMateWorkerTokenizerController.ts b/src/vs/workbench/services/textMate/browser/backgroundTokenization/textMateWorkerTokenizerController.ts index 701579d36269..b42fdfdaf3dc 100644 --- a/src/vs/workbench/services/textMate/browser/backgroundTokenization/textMateWorkerTokenizerController.ts +++ b/src/vs/workbench/services/textMate/browser/backgroundTokenization/textMateWorkerTokenizerController.ts @@ -104,7 +104,7 @@ export class TextMateWorkerTokenizerController extends Disposable { /** * This method is called from the worker through the worker host. */ - public async setTokensAndStates(controllerId: number, versionId: number, rawTokens: ArrayBuffer, stateDeltas: StateDeltas[]): Promise { + public async setTokensAndStates(controllerId: number, versionId: number, rawTokens: Uint8Array, stateDeltas: StateDeltas[]): Promise { if (this.controllerId !== controllerId) { // This event is for an outdated controller (the worker didn't receive the delete/create messages yet), ignore the event. return; diff --git a/src/vs/workbench/services/userDataProfile/browser/snippetsResource.ts b/src/vs/workbench/services/userDataProfile/browser/snippetsResource.ts index 7b29090f272b..547a16908689 100644 --- a/src/vs/workbench/services/userDataProfile/browser/snippetsResource.ts +++ b/src/vs/workbench/services/userDataProfile/browser/snippetsResource.ts @@ -99,7 +99,7 @@ export class SnippetsResource implements IProfileResource { export class SnippetsResourceTreeItem implements IProfileResourceTreeItem { readonly type = ProfileResourceType.Snippets; - readonly handle = this.profile.snippetsHome.toString(); + readonly handle: string; readonly label = { label: localize('snippets', "Snippets") }; readonly collapsibleState = TreeItemCollapsibleState.Collapsed; checkbox: ITreeItemCheckboxState | undefined; @@ -110,7 +110,9 @@ export class SnippetsResourceTreeItem implements IProfileResourceTreeItem { private readonly profile: IUserDataProfile, @IInstantiationService private readonly instantiationService: IInstantiationService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, - ) { } + ) { + this.handle = this.profile.snippetsHome.toString(); + } async getChildren(): Promise { const snippetsResources = await this.instantiationService.createInstance(SnippetsResource).getSnippetsResources(this.profile); diff --git a/src/vs/workbench/services/workspaces/common/workspaceUtils.ts b/src/vs/workbench/services/workspaces/common/workspaceUtils.ts index 4ba2f5af24a7..a67b3457ad0f 100644 --- a/src/vs/workbench/services/workspaces/common/workspaceUtils.ts +++ b/src/vs/workbench/services/workspaces/common/workspaceUtils.ts @@ -2,22 +2,8 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { URI } from '../../../../base/common/uri.js'; import { IWorkspace } from '../../../../platform/workspace/common/workspace.js'; import { IFileService } from '../../../../platform/files/common/files.js'; -import { IStorageService, StorageScope } from '../../../../platform/storage/common/storage.js'; - -export function isChatTransferredWorkspace(workspace: IWorkspace, storageService: IStorageService): boolean { - const workspaceUri = workspace.folders[0]?.uri; - if (!workspaceUri) { - return false; - } - const chatWorkspaceTransfer = storageService.getObject('chat.workspaceTransfer', StorageScope.PROFILE, []); - const toWorkspace: { toWorkspace: URI }[] = chatWorkspaceTransfer.map((item: any) => { - return { toWorkspace: URI.from(item.toWorkspace) }; - }); - return toWorkspace.some(item => item.toWorkspace.toString() === workspaceUri.toString()); -} export async function areWorkspaceFoldersEmpty(workspace: IWorkspace, fileService: IFileService): Promise { for (const folder of workspace.folders) { diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 891747556c9f..34647402a90e 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -1577,7 +1577,7 @@ export class TestHostService implements IHostService { async toggleFullScreen(): Promise { } - async getScreenshot(): Promise { return undefined; } + async getScreenshot(): Promise { return undefined; } async getNativeWindowHandle(_windowId: number): Promise { return undefined; } diff --git a/src/vs/workbench/test/electron-sandbox/workbenchTestServices.ts b/src/vs/workbench/test/electron-sandbox/workbenchTestServices.ts index 9bc958141ff7..a8e01a22f353 100644 --- a/src/vs/workbench/test/electron-sandbox/workbenchTestServices.ts +++ b/src/vs/workbench/test/electron-sandbox/workbenchTestServices.ts @@ -161,7 +161,7 @@ export class TestNativeHostService implements INativeHostService { async hasClipboard(format: string, type?: 'selection' | 'clipboard' | undefined): Promise { return false; } async windowsGetStringRegKey(hive: 'HKEY_CURRENT_USER' | 'HKEY_LOCAL_MACHINE' | 'HKEY_CLASSES_ROOT' | 'HKEY_USERS' | 'HKEY_CURRENT_CONFIG', path: string, name: string): Promise { return undefined; } async profileRenderer(): Promise { throw new Error(); } - async getScreenshot(): Promise { return undefined; } + async getScreenshot(): Promise { return undefined; } } export class TestExtensionTipsService extends AbstractNativeExtensionTipsService { diff --git a/test/automation/src/code.ts b/test/automation/src/code.ts index 75b8a4980ec1..42dcf482dccf 100644 --- a/test/automation/src/code.ts +++ b/test/automation/src/code.ts @@ -29,7 +29,7 @@ export interface LaunchOptions { readonly tracing?: boolean; snapshots?: boolean; readonly headless?: boolean; - readonly browser?: 'chromium' | 'webkit' | 'firefox'; + readonly browser?: 'chromium' | 'webkit' | 'firefox' | 'chromium-msedge' | 'chromium-chrome'; readonly quality: Quality; } @@ -157,24 +157,15 @@ export class Code { // after 5 / 10 seconds: try to exit gracefully again case 10: case 20: { - this.logger.log('Smoke test exit call did not terminate process after 5-10s, gracefully trying to exit the application again...'); + this.logger.log('Smoke test exit() call did not terminate process after 5-10s, gracefully trying to exit the application again...'); this.driver.exitApplication(); break; } // after 20 seconds: forcefully kill case 40: { - this.logger.log('Smoke test exit call did not terminate process after 20s, forcefully exiting the application...'); - - // no need to await since we're polling for the process to die anyways - treekill(pid, err => { - try { - process.kill(pid, 0); // throws an exception if the process doesn't exist anymore - this.logger.log('Failed to kill Electron process tree:', err?.message); - } catch (error) { - // Expected when process is gone - } - }); + this.logger.log('Smoke test exit() call did not terminate process after 20s, forcefully exiting the application...'); + this.kill(pid); // no need to await since we're polling for the process to die anyways break; } @@ -182,16 +173,27 @@ export class Code { // after 30 seconds: give up case 60: { done = true; - this.logger.log('Smoke test exit call did not terminate process after 30s, giving up'); + this.logger.log('Smoke test exit() call did not terminate process after 30s, giving up'); resolve(); } } try { process.kill(pid, 0); // throws an exception if the process doesn't exist anymore. + + const isAlive = await this.driver.isAlive(); + if (!isAlive) { + this.logger.log('Smoke test exit() call did not terminate process, but process is not alive anymore, forcefully exiting the application...'); + this.kill(pid); // no need to await since we're polling for the process to die anyways + } + await this.wait(500); + + process.kill(pid, 0); // throws an exception if the process doesn't exist anymore. } catch (error) { + this.logger.log('Smoke test exit() call terminated process successfully'); done = true; + resolve(); } } @@ -199,6 +201,21 @@ export class Code { }), 'Code#exit()', this.logger); } + private kill(pid: number): Promise { + return new Promise(resolve => { + treekill(pid, err => { + try { + process.kill(pid, 0); // throws an exception if the process doesn't exist anymore + this.logger.log('Failed to kill Electron process tree:', err?.message); + } catch (error) { + // Expected when process is gone + } + + resolve(); + }); + }); + } + async getElement(selector: string): Promise { return (await this.driver.getElements(selector))?.[0]; } diff --git a/test/automation/src/playwrightBrowser.ts b/test/automation/src/playwrightBrowser.ts index 3c52d261051a..f4f63875b2a9 100644 --- a/test/automation/src/playwrightBrowser.ts +++ b/test/automation/src/playwrightBrowser.ts @@ -90,9 +90,11 @@ async function launchServer(options: LaunchOptions) { async function launchBrowser(options: LaunchOptions, endpoint: string) { const { logger, workspacePath, tracing, snapshots, headless } = options; - const browser = await measureAndLog(() => playwright[options.browser ?? 'chromium'].launch({ + const [browserType, browserChannel] = (options.browser ?? 'chromium').split('-'); + const browser = await measureAndLog(() => playwright[browserType as unknown as 'chromium' | 'webkit' | 'firefox'].launch({ headless: headless ?? false, - timeout: 0 + timeout: 0, + channel: browserChannel, }), 'playwright#launch', logger); browser.on('disconnected', () => logger.log(`Playwright: browser disconnected`)); diff --git a/test/automation/src/playwrightDriver.ts b/test/automation/src/playwrightDriver.ts index 018fe7c351aa..8c5102ecc612 100644 --- a/test/automation/src/playwrightDriver.ts +++ b/test/automation/src/playwrightDriver.ts @@ -204,6 +204,9 @@ export class PlaywrightDriver { } // Desktop: exit via `driver.exitApplication` + // + // Note: we are not calling `application.close()` here because from testing we see + // that sometimes this does not result in an orderly shutdown of the application. else { try { await measureAndLog(() => this.evaluateWithDriver(([driver]) => driver.exitApplication()), 'driver.exitApplication()', this.options.logger); @@ -329,6 +332,15 @@ export class PlaywrightDriver { private async getDriverHandle(): Promise> { return this.page.evaluateHandle('window.driver'); } + + async isAlive(): Promise { + try { + await this.getDriverHandle(); + return true; + } catch (error) { + return false; + } + } } export function wait(ms: number): Promise { diff --git a/test/integration/browser/src/index.ts b/test/integration/browser/src/index.ts index 2613f10da628..0c3cd8efd320 100644 --- a/test/integration/browser/src/index.ts +++ b/test/integration/browser/src/index.ts @@ -49,7 +49,7 @@ if (args.help) { --workspacePath Path to the workspace (folder or *.code-workspace file) to open in the test --extensionDevelopmentPath Path to the extension to test --extensionTestsPath Path to the extension tests - --browser Browser in which integration tests should run + --browser Browser in which integration tests should run. separate the channel with a dash, e.g. 'chromium-msedge' or 'chromium-chrome' --debug Do not run browsers headless --help Print this help message `); @@ -61,9 +61,10 @@ const width = 1200; const height = 800; type BrowserType = 'chromium' | 'firefox' | 'webkit'; +type BrowserChannel = 'msedge' | 'chrome'; -async function runTestsInBrowser(browserType: BrowserType, endpoint: url.UrlWithStringQuery, server: cp.ChildProcess): Promise { - const browser = await playwright[browserType].launch({ headless: !Boolean(args.debug) }); +async function runTestsInBrowser(browserType: BrowserType, browserChannel: BrowserChannel, endpoint: url.UrlWithStringQuery, server: cp.ChildProcess): Promise { + const browser = await playwright[browserType].launch({ headless: !Boolean(args.debug), channel: browserChannel }); const context = await browser.newContext(); const page = await context.newPage(); @@ -154,7 +155,7 @@ function consoleLogFn(msg: playwright.ConsoleMessage) { return console.log; } -async function launchServer(browserType: BrowserType): Promise<{ endpoint: url.UrlWithStringQuery; server: cp.ChildProcess }> { +async function launchServer(browserType: BrowserType, browserChannel: BrowserChannel): Promise<{ endpoint: url.UrlWithStringQuery; server: cp.ChildProcess }> { // Ensure a tmp user-data-dir is used for the tests const tmpDir = tmp.dirSync({ prefix: 't' }); @@ -164,7 +165,7 @@ async function launchServer(browserType: BrowserType): Promise<{ endpoint: url.U const userDataDir = path.join(testDataPath, 'd'); const env = { - VSCODE_BROWSER: browserType, + VSCODE_BROWSER: browserChannel ? `${browserType}-${browserChannel}` : browserType, ...process.env }; @@ -224,8 +225,9 @@ async function launchServer(browserType: BrowserType): Promise<{ endpoint: url.U }); } -launchServer(args.browser).then(async ({ endpoint, server }) => { - return runTestsInBrowser(args.browser, endpoint, server); +const [browserType, browserChannel] = args.browser.split('-'); +launchServer(browserType, browserChannel).then(async ({ endpoint, server }) => { + return runTestsInBrowser(browserType, browserChannel, endpoint, server); }, error => { console.error(error); process.exit(1); diff --git a/test/unit/browser/index.js b/test/unit/browser/index.js index f19c21e46c47..139fba95514b 100644 --- a/test/unit/browser/index.js +++ b/test/unit/browser/index.js @@ -76,7 +76,7 @@ Options: --grep, -g, -f only run tests matching --debug, --debug-browser do not run browsers headless --sequential only run suites for a single browser at a time ---browser browsers in which tests should run +--browser browsers in which tests should run. separate the channel with a dash, e.g. 'chromium-msedge' or 'chromium-chrome' --reporter the mocha reporter --reporter-options the mocha reporter options --tfs tfs @@ -239,9 +239,9 @@ async function createServer() { }); } -async function runTestsInBrowser(testModules, browserType) { +async function runTestsInBrowser(testModules, browserType, browserChannel) { const server = await createServer(); - const browser = await playwright[browserType].launch({ headless: !Boolean(args.debug), devtools: Boolean(args.debug) }); + const browser = await playwright[browserType].launch({ headless: !Boolean(args.debug), devtools: Boolean(args.debug), channel: browserChannel }); const context = await browser.newContext(); const page = await context.newPage(); const target = new URL(server.url + '/test/unit/browser/renderer.html'); @@ -281,7 +281,7 @@ async function runTestsInBrowser(testModules, browserType) { consoleLogFn(msg)(msg.text(), await Promise.all(msg.args().map(async arg => await arg.jsonValue()))); }); - withReporter(browserType, new EchoRunner(emitter, browserType.toUpperCase())); + withReporter(browserType, new EchoRunner(emitter, browserChannel ? `${browserType.toUpperCase()}-${browserChannel.toUpperCase()}` : browserType.toUpperCase())); // collection failures for console printing const failingModuleIds = []; @@ -382,7 +382,7 @@ class EchoRunner extends events.EventEmitter { testModules.then(async modules => { // run tests in selected browsers - const browserTypes = Array.isArray(args.browser) + const browsers = Array.isArray(args.browser) ? args.browser : [args.browser]; let messages = []; @@ -390,12 +390,14 @@ testModules.then(async modules => { try { if (args.sequential) { - for (const browserType of browserTypes) { - messages.push(await runTestsInBrowser(modules, browserType)); + for (const browser of browsers) { + const [browserType, browserChannel] = browser.split('-'); + messages.push(await runTestsInBrowser(modules, browserType, browserChannel)); } } else { - messages = await Promise.all(browserTypes.map(async browserType => { - return await runTestsInBrowser(modules, browserType); + messages = await Promise.all(browsers.map(async browser => { + const [browserType, browserChannel] = browser.split('-'); + return await runTestsInBrowser(modules, browserType, browserChannel); })); } } catch (err) {